mirror of
https://github.com/krateng/maloja.git
synced 2025-06-06 10:23:29 +03:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9e44cc3ce6 | ||
|
c55ed87b52 | ||
|
3ba27ffc37 | ||
|
300e2c1ff7 | ||
|
5c343053d9 | ||
|
7dab61e420 | ||
|
5296960d68 | ||
|
e060241acb | ||
|
c571ffbf07 | ||
|
767a6bca26 | ||
|
ffed0c29b0 | ||
|
ca65813619 | ||
|
5926dc3307 | ||
|
811bc16a3f | ||
|
a4ec29dd4c | ||
|
a8293063a5 | ||
|
126d155208 | ||
|
7f774f03c4 | ||
|
cc64c894f0 | ||
|
76c013e130 | ||
|
9a6c51a36d | ||
|
e3a578da2f | ||
|
a851e36485 | ||
|
0e928b4007 | ||
|
63386b5ede | ||
|
9d21800eb9 | ||
|
5a95d4e056 | ||
|
968bea14d9 | ||
|
5e62ccc254 | ||
|
273713cdc4 | ||
|
f8b10ab68c | ||
|
922eae7b68 | ||
|
cf0a856040 | ||
|
26f26f36cb | ||
|
1462883ab5 | ||
|
a0b83be095 | ||
|
2750241e61 |
1
.github/workflows/docker.yml
vendored
1
.github/workflows/docker.yml
vendored
@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
- 'runaction-docker'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push_to_registry:
|
push_to_registry:
|
||||||
|
8
.github/workflows/pypi.yml
vendored
8
.github/workflows/pypi.yml
vendored
@ -4,11 +4,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
- 'runaction-pypi'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish_to_pypi:
|
publish_to_pypi:
|
||||||
name: Push Package to PyPI
|
name: Push Package to PyPI
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
|
||||||
@ -25,7 +28,4 @@ jobs:
|
|||||||
run: python -m build
|
run: python -m build
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@b7f401de30cb6434a1e19f805ff006643653240e
|
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70
|
||||||
with:
|
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
# environments / builds
|
# environments / builds
|
||||||
.venv/*
|
.venv/*
|
||||||
testdata*
|
|
||||||
/dist
|
/dist
|
||||||
/build
|
/build
|
||||||
/*.egg-info
|
/*.egg-info
|
||||||
|
36
APKBUILD
36
APKBUILD
@ -1,36 +0,0 @@
|
|||||||
# Contributor: Johannes Krattenmacher <maloja@dev.krateng.ch>
|
|
||||||
# Maintainer: Johannes Krattenmacher <maloja@dev.krateng.ch>
|
|
||||||
pkgname=maloja
|
|
||||||
pkgver=3.0.0-dev
|
|
||||||
pkgrel=0
|
|
||||||
pkgdesc="Self-hosted music scrobble database"
|
|
||||||
url="https://github.com/krateng/maloja"
|
|
||||||
arch="noarch"
|
|
||||||
license="GPL-3.0"
|
|
||||||
depends="python3 tzdata"
|
|
||||||
pkgusers=$pkgname
|
|
||||||
pkggroups=$pkgname
|
|
||||||
depends_dev="gcc g++ python3-dev libxml2-dev libxslt-dev libffi-dev libc-dev py3-pip linux-headers"
|
|
||||||
makedepends="$depends_dev"
|
|
||||||
source="
|
|
||||||
$pkgname-$pkgver.tar.gz::https://github.com/krateng/maloja/archive/refs/tags/v$pkgver.tar.gz
|
|
||||||
"
|
|
||||||
builddir="$srcdir"/$pkgname-$pkgver
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd $builddir
|
|
||||||
python3 -m build .
|
|
||||||
pip3 install dist/*.tar.gz
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
mkdir -p /etc/$pkgname || return 1
|
|
||||||
mkdir -p /var/lib/$pkgname || return 1
|
|
||||||
mkdir -p /var/cache/$pkgname || return 1
|
|
||||||
mkdir -p /var/logs/$pkgname || return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
sha512sums="a674eaaaa248fc2b315514d79f9a7a0bac6aa1582fe29554d9176e8b551e8aa3aa75abeebdd7713e9e98cc987e7bd57dc7a5e9a2fb85af98b9c18cb54de47bf7 $pkgname-${pkgver}.tar.gz"
|
|
@ -1,4 +1,4 @@
|
|||||||
FROM lsiobase/alpine:3.19 as base
|
FROM lsiobase/alpine:3.21 AS base
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ RUN \
|
|||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
python3 \
|
python3 \
|
||||||
py3-lxml \
|
py3-lxml \
|
||||||
|
libmagic \
|
||||||
tzdata && \
|
tzdata && \
|
||||||
echo "" && \
|
echo "" && \
|
||||||
echo "**** install pip dependencies ****" && \
|
echo "**** install pip dependencies ****" && \
|
||||||
|
@ -9,49 +9,14 @@ Clone the repository and enter it.
|
|||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
To avoid cluttering your system, consider using a [virtual environment](https://docs.python.org/3/tutorial/venv.html).
|
To avoid cluttering your system, consider using a [virtual environment](https://docs.python.org/3/tutorial/venv.html), or better yet run the included `docker-compose.yml` file.
|
||||||
|
Your IDE should let you run the file directly, otherwise you can execute `docker compose -f dev/docker-compose.yml -p maloja up --force-recreate --build`.
|
||||||
Your system needs several packages installed. For supported distributions, this can be done with e.g.
|
|
||||||
|
|
||||||
```console
|
|
||||||
sh ./install/install_dependencies_alpine.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
For other distros, try to find the equivalents of the packages listed or simply check your error output.
|
|
||||||
|
|
||||||
Then install all Python dependencies with
|
|
||||||
|
|
||||||
```console
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Running the server
|
## Running the server
|
||||||
|
|
||||||
For development, you might not want to install maloja files all over your filesystem. Use the environment variable `MALOJA_DATA_DIRECTORY` to force all user files into one central directory - this way, you can also quickly change between multiple configurations.
|
Use the environment variable `MALOJA_DATA_DIRECTORY` to force all user files into one central directory - this way, you can also quickly change between multiple configurations.
|
||||||
|
|
||||||
You can quickly run the server with all your local changes with
|
|
||||||
|
|
||||||
```console
|
|
||||||
python3 -m maloja run
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also build the package with
|
|
||||||
|
|
||||||
```console
|
|
||||||
pip install .
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
You can also always build and run the server with
|
|
||||||
|
|
||||||
```console
|
|
||||||
sh ./dev/run_docker.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This will use the directory `testdata`.
|
|
||||||
|
|
||||||
## Further help
|
## Further help
|
||||||
|
|
||||||
|
64
README.md
64
README.md
@ -14,10 +14,6 @@ You can check [my own Maloja page](https://maloja.krateng.ch) as an example inst
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
* [Features](#features)
|
* [Features](#features)
|
||||||
* [How to install](#how-to-install)
|
* [How to install](#how-to-install)
|
||||||
* [Requirements](#requirements)
|
|
||||||
* [Docker / Podman](#docker--podman)
|
|
||||||
* [PyPI](#pypi)
|
|
||||||
* [From Source](#from-source)
|
|
||||||
* [Extras](#extras)
|
* [Extras](#extras)
|
||||||
* [How to use](#how-to-use)
|
* [How to use](#how-to-use)
|
||||||
* [Basic control](#basic-control)
|
* [Basic control](#basic-control)
|
||||||
@ -40,15 +36,8 @@ You can check [my own Maloja page](https://maloja.krateng.ch) as an example inst
|
|||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
### Requirements
|
To avoid issues with version / dependency mismatches, Maloja should only be used in **Docker** or **Podman**, not on bare metal.
|
||||||
|
I cannot offer any help for bare metal installations (but using venv should help).
|
||||||
Maloja should run on any x86 or ARM machine that runs Python.
|
|
||||||
|
|
||||||
It is highly recommended to use **Docker** or **Podman**.
|
|
||||||
|
|
||||||
Your CPU should have a single core passmark score of at the very least 1500. 500 MB RAM should give you a decent experience, but performance will benefit greatly from up to 2 GB.
|
|
||||||
|
|
||||||
### Docker / Podman
|
|
||||||
|
|
||||||
Pull the [latest image](https://hub.docker.com/r/krateng/maloja) or check out the repository and use the included Containerfile.
|
Pull the [latest image](https://hub.docker.com/r/krateng/maloja) or check out the repository and use the included Containerfile.
|
||||||
|
|
||||||
@ -67,11 +56,7 @@ An example of a minimum run configuration to access maloja via `localhost:42010`
|
|||||||
docker run -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
docker run -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Linux Host
|
If you are using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_) the following DOES NOT apply to you, but if you are running **Docker** on a **Linux Host** you should specify `user:group` ids of the user who owns the folder on the host machine bound to `MALOJA_DATA_DIRECTORY` in order to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
|
||||||
|
|
||||||
**NOTE:** If you are using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_) this DOES NOT apply to you.
|
|
||||||
|
|
||||||
If you are running Docker on a **Linux Host** you should specify `user:group` ids of the user who owns the folder on the host machine bound to `MALOJA_DATA_DIRECTORY` in order to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
|
|
||||||
|
|
||||||
To get the UID and GID for the current user run these commands from a terminal:
|
To get the UID and GID for the current user run these commands from a terminal:
|
||||||
|
|
||||||
@ -84,33 +69,6 @@ The modified run command with these variables would look like:
|
|||||||
docker run -e PUID=1000 -e PGID=1001 -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
docker run -e PUID=1000 -e PGID=1001 -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||||
```
|
```
|
||||||
|
|
||||||
### PyPI
|
|
||||||
|
|
||||||
You can install Maloja with
|
|
||||||
|
|
||||||
```console
|
|
||||||
pip install malojaserver
|
|
||||||
```
|
|
||||||
|
|
||||||
To make sure all dependencies are installed, you can also use one of the included scripts in the `install` folder.
|
|
||||||
|
|
||||||
### From Source
|
|
||||||
|
|
||||||
Clone this repository and enter the directory with
|
|
||||||
|
|
||||||
```console
|
|
||||||
git clone https://github.com/krateng/maloja
|
|
||||||
cd maloja
|
|
||||||
```
|
|
||||||
|
|
||||||
Then install all the requirements and build the package, e.g.:
|
|
||||||
|
|
||||||
```console
|
|
||||||
sh ./install/install_dependencies_alpine.sh
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install .
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Extras
|
### Extras
|
||||||
|
|
||||||
@ -123,30 +81,18 @@ Then install all the requirements and build the package, e.g.:
|
|||||||
|
|
||||||
### Basic control
|
### Basic control
|
||||||
|
|
||||||
When not running in a container, you can run the application with `maloja run`. You can also run it in the background with
|
When not running in a container, you can run the application with `maloja run`.
|
||||||
`maloja start` and `maloja stop`, but this might not be supported in the future.
|
|
||||||
|
|
||||||
|
|
||||||
### Data
|
### Data
|
||||||
|
|
||||||
If you would like to import your previous scrobbles, use the command `maloja import *filename*`. This works on:
|
If you would like to import your previous scrobbles, copy them into the import folder in your data directory. This works on:
|
||||||
|
|
||||||
* a Last.fm export generated by [ghan64's website](https://lastfm.ghan.nl/export/)
|
* a Last.fm export generated by [ghan64's website](https://lastfm.ghan.nl/export/)
|
||||||
* an official [Spotify data export file](https://www.spotify.com/us/account/privacy/)
|
* an official [Spotify data export file](https://www.spotify.com/us/account/privacy/)
|
||||||
* an official [ListenBrainz export file](https://listenbrainz.org/profile/export/)
|
* an official [ListenBrainz export file](https://listenbrainz.org/profile/export/)
|
||||||
* the export of another Maloja instance
|
* the export of another Maloja instance
|
||||||
|
|
||||||
⚠️ Never import your data while maloja is running. When you need to do import inside docker container start it in shell mode instead and perform import before starting the container as mentioned above.
|
|
||||||
|
|
||||||
```console
|
|
||||||
docker run -it --entrypoint sh -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
|
||||||
cd /mljdata
|
|
||||||
maloja import my_last_fm_export.csv
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
To backup your data, run `maloja backup`, optional with `--include_images`.
|
|
||||||
|
|
||||||
### Customization
|
### Customization
|
||||||
|
|
||||||
* Have a look at the [available settings](settings.md) and specifiy your choices in `/etc/maloja/settings.ini`. You can also set each of these settings as an environment variable with the prefix `MALOJA_` (e.g. `MALOJA_SKIP_SETUP`).
|
* Have a look at the [available settings](settings.md) and specifiy your choices in `/etc/maloja/settings.ini`. You can also set each of these settings as an environment variable with the prefix `MALOJA_` (e.g. `MALOJA_SKIP_SETUP`).
|
||||||
|
3
dev/clear_testdata.sh
Normal file
3
dev/clear_testdata.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
sudo rm -r ./testdata
|
||||||
|
mkdir ./testdata
|
||||||
|
chmod 777 ./testdata
|
13
dev/docker-compose.yml
Normal file
13
dev/docker-compose.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
maloja:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: ./Containerfile
|
||||||
|
ports:
|
||||||
|
- "42010:42010"
|
||||||
|
volumes:
|
||||||
|
- "./testdata:/data"
|
||||||
|
environment:
|
||||||
|
- "MALOJA_DATA_DIRECTORY=/data"
|
||||||
|
- "PUID=1000"
|
||||||
|
- "PGID=1000"
|
@ -1,21 +0,0 @@
|
|||||||
import toml
|
|
||||||
import os
|
|
||||||
|
|
||||||
with open("pyproject.toml") as filed:
|
|
||||||
data = toml.load(filed)
|
|
||||||
|
|
||||||
info = {
|
|
||||||
'name':data['project']['name'],
|
|
||||||
'license':"GPLv3",
|
|
||||||
'version':data['project']['version'],
|
|
||||||
'architecture':'all',
|
|
||||||
'description':'"' + data['project']['description'] + '"',
|
|
||||||
'url':'"' + data['project']['urls']['homepage'] + '"',
|
|
||||||
'maintainer':f"\"{data['project']['authors'][0]['name']} <{data['project']['authors'][0]['email']}>\"",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for target in ["apk","deb"]:
|
|
||||||
lcmd = f"fpm {' '.join(f'--{key} {info[key]}' for key in info)} -s python -t {target} . "
|
|
||||||
print(lcmd)
|
|
||||||
os.system(lcmd)
|
|
@ -39,8 +39,19 @@ minor_release_name: "Nicole"
|
|||||||
- "[Feature] Added option to show scrobbles on tile charts"
|
- "[Feature] Added option to show scrobbles on tile charts"
|
||||||
- "[Bugfix] Fixed Last.fm authentication"
|
- "[Bugfix] Fixed Last.fm authentication"
|
||||||
3.2.3:
|
3.2.3:
|
||||||
|
commit: "a7dcd3df8a6b051a1f6d0b7d10cc5af83502445c"
|
||||||
notes:
|
notes:
|
||||||
- "[Architecture] Upgraded doreah, significant rework of authentication"
|
- "[Architecture] Upgraded doreah, significant rework of authentication"
|
||||||
- "[Bugfix] Fixed initial permission check"
|
- "[Bugfix] Fixed initial permission check"
|
||||||
- "[Bugfix] Fixed and updated various texts"
|
- "[Bugfix] Fixed and updated various texts"
|
||||||
- "[Bugfix] Fixed moving tracks to different album"
|
- "[Bugfix] Fixed moving tracks to different album"
|
||||||
|
3.2.4:
|
||||||
|
notes:
|
||||||
|
- "[Architecture] Removed daemonization capabilities"
|
||||||
|
- "[Architecture] Moved import to main server process"
|
||||||
|
- "[Feature] Implemented support for ghan's csv Last.fm export"
|
||||||
|
- "[Performance] Debounced search"
|
||||||
|
- "[Bugfix] Fixed stuck scrobbling from Navidrome"
|
||||||
|
- "[Bugfix] Fixed missing image mimetype"
|
||||||
|
- "[Technical] Pinned dependencies"
|
||||||
|
- "[Technical] Upgraded Python and Alpine"
|
@ -1,2 +0,0 @@
|
|||||||
docker build -t maloja . -f Containerfile
|
|
||||||
docker run --rm -p 42010:42010 -v $PWD/testdata:/mlj -e MALOJA_DATA_DIRECTORY=/mlj maloja
|
|
@ -1,2 +0,0 @@
|
|||||||
podman build -t maloja . -f Containerfile
|
|
||||||
podman run --rm -p 42010:42010 -v $PWD/testdata:/mlj -e MALOJA_DATA_DIRECTORY=/mlj maloja
|
|
@ -1,36 +0,0 @@
|
|||||||
# Contributor: Johannes Krattenmacher <maloja@dev.krateng.ch>
|
|
||||||
# Maintainer: Johannes Krattenmacher <maloja@dev.krateng.ch>
|
|
||||||
pkgname={{ tool.flit.module.name }}
|
|
||||||
pkgver={{ project.version }}
|
|
||||||
pkgrel=0
|
|
||||||
pkgdesc="{{ project.description }}"
|
|
||||||
url="{{ project.urls.homepage }}"
|
|
||||||
arch="noarch"
|
|
||||||
license="GPL-3.0"
|
|
||||||
depends="{{ tool.osreqs.alpine.run | join(' ') }}"
|
|
||||||
pkgusers=$pkgname
|
|
||||||
pkggroups=$pkgname
|
|
||||||
depends_dev="{{ tool.osreqs.alpine.build | join(' ') }}"
|
|
||||||
makedepends="$depends_dev"
|
|
||||||
source="
|
|
||||||
$pkgname-$pkgver.tar.gz::{{ project.urls.repository }}/archive/refs/tags/v$pkgver.tar.gz
|
|
||||||
"
|
|
||||||
builddir="$srcdir"/$pkgname-$pkgver
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd $builddir
|
|
||||||
python3 -m build .
|
|
||||||
pip3 install dist/*.tar.gz
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
mkdir -p /etc/$pkgname || return 1
|
|
||||||
mkdir -p /var/lib/$pkgname || return 1
|
|
||||||
mkdir -p /var/cache/$pkgname || return 1
|
|
||||||
mkdir -p /var/logs/$pkgname || return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
sha512sums="a674eaaaa248fc2b315514d79f9a7a0bac6aa1582fe29554d9176e8b551e8aa3aa75abeebdd7713e9e98cc987e7bd57dc7a5e9a2fb85af98b9c18cb54de47bf7 $pkgname-${pkgver}.tar.gz"
|
|
@ -1,40 +0,0 @@
|
|||||||
FROM alpine:3.15
|
|
||||||
# Python image includes two Python versions, so use base Alpine
|
|
||||||
|
|
||||||
# Based on the work of Jonathan Boeckel <jonathanboeckel1996@gmail.com>
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
# Install run dependencies first
|
|
||||||
RUN apk add --no-cache {{ tool.osreqs.alpine.run | join(' ') }}
|
|
||||||
|
|
||||||
# system pip could be removed after build, but apk then decides to also remove all its
|
|
||||||
# python dependencies, even if they are explicitly installed as python packages
|
|
||||||
# whut
|
|
||||||
RUN \
|
|
||||||
apk add py3-pip && \
|
|
||||||
pip install wheel
|
|
||||||
|
|
||||||
|
|
||||||
COPY ./requirements.txt ./requirements.txt
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
apk add --no-cache --virtual .build-deps {{ tool.osreqs.alpine.build | join(' ') }} && \
|
|
||||||
pip install --no-cache-dir -r requirements.txt && \
|
|
||||||
apk del .build-deps
|
|
||||||
|
|
||||||
|
|
||||||
# no chance for caching below here
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN pip install /usr/src/app
|
|
||||||
|
|
||||||
# Docker-specific configuration
|
|
||||||
# defaulting to IPv4 is no longer necessary (default host is dual stack)
|
|
||||||
ENV MALOJA_SKIP_SETUP=yes
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
EXPOSE 42010
|
|
||||||
# use exec form for better signal handling https://docs.docker.com/engine/reference/builder/#entrypoint
|
|
||||||
ENTRYPOINT ["maloja", "run"]
|
|
@ -1,4 +0,0 @@
|
|||||||
{% include 'install/install_dependencies_alpine.sh.jinja' %}
|
|
||||||
apk add py3-pip
|
|
||||||
pip install wheel
|
|
||||||
pip install malojaserver
|
|
@ -1,4 +0,0 @@
|
|||||||
{% include 'install/install_dependencies_debian.sh.jinja' %}
|
|
||||||
apt install python3-pip
|
|
||||||
pip install wheel
|
|
||||||
pip install malojaserver
|
|
@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
apk update
|
|
||||||
apk add \
|
|
||||||
{{ (tool.osreqs.alpine.build + tool.osreqs.alpine.run + tool.osreqs.alpine.opt) | join(' \\\n\t') }}
|
|
@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
apt update
|
|
||||||
apt install \
|
|
||||||
{{ (tool.osreqs.debian.build + tool.osreqs.debian.run + tool.osreqs.debian.opt) | join(' \\\n\t') }}
|
|
@ -1,17 +1,21 @@
|
|||||||
|
"""
|
||||||
|
Create necessary files from sources of truth. Currently just the requirements.txt files.
|
||||||
|
"""
|
||||||
|
|
||||||
import toml
|
import toml
|
||||||
import os
|
import os
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
env = jinja2.Environment(
|
env = jinja2.Environment(
|
||||||
loader=jinja2.FileSystemLoader('dev/templates'),
|
loader=jinja2.FileSystemLoader('./templates'),
|
||||||
autoescape=jinja2.select_autoescape(['html', 'xml']),
|
autoescape=jinja2.select_autoescape(['html', 'xml']),
|
||||||
keep_trailing_newline=True
|
keep_trailing_newline=True
|
||||||
)
|
)
|
||||||
|
|
||||||
with open("pyproject.toml") as filed:
|
with open("../pyproject.toml") as filed:
|
||||||
data = toml.load(filed)
|
data = toml.load(filed)
|
||||||
|
|
||||||
templatedir = "./dev/templates"
|
templatedir = "./templates"
|
||||||
|
|
||||||
for root,dirs,files in os.walk(templatedir):
|
for root,dirs,files in os.walk(templatedir):
|
||||||
|
|
||||||
@ -23,7 +27,7 @@ for root,dirs,files in os.walk(templatedir):
|
|||||||
if not f.endswith('.jinja'): continue
|
if not f.endswith('.jinja'): continue
|
||||||
|
|
||||||
srcfile = os.path.join(root,f)
|
srcfile = os.path.join(root,f)
|
||||||
trgfile = os.path.join(reldirpath,f.replace(".jinja",""))
|
trgfile = os.path.join("..", reldirpath,f.replace(".jinja",""))
|
||||||
|
|
||||||
|
|
||||||
template = env.get_template(relfilepath)
|
template = env.get_template(relfilepath)
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Read the changelogs / version metadata and create all git tags
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import yaml
|
import yaml
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
apk update
|
|
||||||
apk add \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
python3-dev \
|
|
||||||
libxml2-dev \
|
|
||||||
libxslt-dev \
|
|
||||||
libffi-dev \
|
|
||||||
libc-dev \
|
|
||||||
py3-pip \
|
|
||||||
linux-headers \
|
|
||||||
python3 \
|
|
||||||
py3-lxml \
|
|
||||||
tzdata \
|
|
||||||
vips
|
|
||||||
|
|
||||||
apk add py3-pip
|
|
||||||
pip install wheel
|
|
||||||
pip install malojaserver
|
|
@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
apt update
|
|
||||||
apt install \
|
|
||||||
python3-pip \
|
|
||||||
python3
|
|
||||||
|
|
||||||
apt install python3-pip
|
|
||||||
pip install wheel
|
|
||||||
pip install malojaserver
|
|
@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
apk update
|
|
||||||
apk add \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
python3-dev \
|
|
||||||
libxml2-dev \
|
|
||||||
libxslt-dev \
|
|
||||||
libffi-dev \
|
|
||||||
libc-dev \
|
|
||||||
py3-pip \
|
|
||||||
linux-headers \
|
|
||||||
python3 \
|
|
||||||
py3-lxml \
|
|
||||||
tzdata \
|
|
||||||
vips
|
|
@ -1,15 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
pacman -Syu
|
|
||||||
pacman -S --needed \
|
|
||||||
gcc \
|
|
||||||
python3 \
|
|
||||||
libxml2 \
|
|
||||||
libxslt \
|
|
||||||
libffi \
|
|
||||||
glibc \
|
|
||||||
python-pip \
|
|
||||||
linux-headers \
|
|
||||||
python \
|
|
||||||
python-lxml \
|
|
||||||
tzdata \
|
|
||||||
libvips
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
apt update
|
|
||||||
apt install \
|
|
||||||
python3-pip \
|
|
||||||
python3
|
|
@ -26,77 +26,6 @@ def print_header_info():
|
|||||||
#print("#####")
|
#print("#####")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_instance():
|
|
||||||
try:
|
|
||||||
return int(subprocess.check_output(["pgrep","-f","maloja$"]))
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_instance_supervisor():
|
|
||||||
try:
|
|
||||||
return int(subprocess.check_output(["pgrep","-f","maloja_supervisor"]))
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def restart():
|
|
||||||
if stop():
|
|
||||||
start()
|
|
||||||
else:
|
|
||||||
print(col["red"]("Could not stop Maloja!"))
|
|
||||||
|
|
||||||
def start():
|
|
||||||
if get_instance_supervisor() is not None:
|
|
||||||
print("Maloja is already running.")
|
|
||||||
else:
|
|
||||||
print_header_info()
|
|
||||||
setup()
|
|
||||||
try:
|
|
||||||
#p = subprocess.Popen(["python3","-m","maloja.server"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
|
||||||
sp = subprocess.Popen(["python3","-m","maloja","supervisor"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
|
||||||
print(col["green"]("Maloja started!"))
|
|
||||||
|
|
||||||
port = conf.malojaconfig["PORT"]
|
|
||||||
|
|
||||||
print("Visit your server address (Port " + str(port) + ") to see your web interface. Visit /admin_setup to get started.")
|
|
||||||
print("If you're installing this on your local machine, these links should get you there:")
|
|
||||||
print("\t" + col["blue"]("http://localhost:" + str(port)))
|
|
||||||
print("\t" + col["blue"]("http://localhost:" + str(port) + "/admin_setup"))
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
print("Error while starting Maloja.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def stop():
|
|
||||||
|
|
||||||
for attempt in [(signal.SIGTERM,2),(signal.SIGTERM,5),(signal.SIGKILL,3),(signal.SIGKILL,5)]:
|
|
||||||
|
|
||||||
pid_sv = get_instance_supervisor()
|
|
||||||
pid = get_instance()
|
|
||||||
|
|
||||||
if pid is None and pid_sv is None:
|
|
||||||
print("Maloja stopped!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
if pid_sv is not None:
|
|
||||||
os.kill(pid_sv,attempt[0])
|
|
||||||
if pid is not None:
|
|
||||||
os.kill(pid,attempt[0])
|
|
||||||
|
|
||||||
time.sleep(attempt[1])
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
print("Maloja stopped!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def onlysetup():
|
def onlysetup():
|
||||||
print_header_info()
|
print_header_info()
|
||||||
setup()
|
setup()
|
||||||
@ -109,24 +38,6 @@ def run_server():
|
|||||||
from . import server
|
from . import server
|
||||||
server.run_server()
|
server.run_server()
|
||||||
|
|
||||||
def run_supervisor():
|
|
||||||
setproctitle("maloja_supervisor")
|
|
||||||
while True:
|
|
||||||
log("Maloja is not running, starting...",module="supervisor")
|
|
||||||
try:
|
|
||||||
process = subprocess.Popen(
|
|
||||||
["python3", "-m", "maloja","run"],
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log("Error starting Maloja: " + str(e),module="supervisor")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
process.wait()
|
|
||||||
except Exception as e:
|
|
||||||
log("Maloja crashed: " + str(e),module="supervisor")
|
|
||||||
|
|
||||||
def debug():
|
def debug():
|
||||||
os.environ["MALOJA_DEV_MODE"] = 'true'
|
os.environ["MALOJA_DEV_MODE"] = 'true'
|
||||||
conf.malojaconfig.load_environment()
|
conf.malojaconfig.load_environment()
|
||||||
@ -139,6 +50,7 @@ def print_info():
|
|||||||
print(col['lightblue']("Log Directory: "),conf.dir_settings['logs'])
|
print(col['lightblue']("Log Directory: "),conf.dir_settings['logs'])
|
||||||
print(col['lightblue']("Network: "),f"Dual Stack, Port {conf.malojaconfig['port']}" if conf.malojaconfig['host'] == "*" else f"IPv{ip_address(conf.malojaconfig['host']).version}, Port {conf.malojaconfig['port']}")
|
print(col['lightblue']("Network: "),f"Dual Stack, Port {conf.malojaconfig['port']}" if conf.malojaconfig['host'] == "*" else f"IPv{ip_address(conf.malojaconfig['host']).version}, Port {conf.malojaconfig['port']}")
|
||||||
print(col['lightblue']("Timezone: "),f"UTC{conf.malojaconfig['timezone']:+d}")
|
print(col['lightblue']("Timezone: "),f"UTC{conf.malojaconfig['timezone']:+d}")
|
||||||
|
print(col['lightblue']("Location Timezone: "),conf.malojaconfig['location_timezone'])
|
||||||
print()
|
print()
|
||||||
try:
|
try:
|
||||||
from importlib.metadata import distribution
|
from importlib.metadata import distribution
|
||||||
@ -173,11 +85,7 @@ def main(*args,**kwargs):
|
|||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
# server
|
# server
|
||||||
"start":start,
|
|
||||||
"restart":restart,
|
|
||||||
"stop":stop,
|
|
||||||
"run":run_server,
|
"run":run_server,
|
||||||
"supervisor":run_supervisor,
|
|
||||||
"debug":debug,
|
"debug":debug,
|
||||||
"setup":onlysetup,
|
"setup":onlysetup,
|
||||||
# admin scripts
|
# admin scripts
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
# you know what f*ck it
|
# you know what f*ck it
|
||||||
# this is hardcoded for now because of that damn project / package name discrepancy
|
# this is hardcoded for now because of that damn project / package name discrepancy
|
||||||
# i'll fix it one day
|
# i'll fix it one day
|
||||||
VERSION = "3.2.3"
|
VERSION = "3.2.4"
|
||||||
HOMEPAGE = "https://github.com/krateng/maloja"
|
HOMEPAGE = "https://github.com/krateng/maloja"
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from ._exceptions import *
|
|||||||
from .. import database
|
from .. import database
|
||||||
import datetime
|
import datetime
|
||||||
from ._apikeys import apikeystore
|
from ._apikeys import apikeystore
|
||||||
from ..database.exceptions import DuplicateScrobble
|
from ..database.exceptions import DuplicateScrobble, DuplicateTimestamp
|
||||||
|
|
||||||
from ..pkg_global.conf import malojaconfig
|
from ..pkg_global.conf import malojaconfig
|
||||||
|
|
||||||
@ -27,6 +27,7 @@ class Listenbrainz(APIHandler):
|
|||||||
InvalidMethodException: (200, {"code": 200, "error": "Invalid Method"}),
|
InvalidMethodException: (200, {"code": 200, "error": "Invalid Method"}),
|
||||||
MalformedJSONException: (400, {"code": 400, "error": "Invalid JSON document submitted."}),
|
MalformedJSONException: (400, {"code": 400, "error": "Invalid JSON document submitted."}),
|
||||||
DuplicateScrobble: (200, {"status": "ok"}),
|
DuplicateScrobble: (200, {"status": "ok"}),
|
||||||
|
DuplicateTimestamp: (409, {"error": "Scrobble with the same timestamp already exists."}),
|
||||||
Exception: (500, {"code": 500, "error": "Unspecified server error."})
|
Exception: (500, {"code": 500, "error": "Unspecified server error."})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
# server
|
# server
|
||||||
from bottle import request, response, FormsDict
|
from bottle import request, response, FormsDict
|
||||||
|
|
||||||
|
from ..pkg_global import conf
|
||||||
|
|
||||||
|
|
||||||
# decorator that makes sure this function is only run in normal operation,
|
# decorator that makes sure this function is only run in normal operation,
|
||||||
# not when we run a task that needs to access the database
|
# not when we run a task that needs to access the database
|
||||||
@ -932,6 +934,9 @@ def get_predefined_rulesets(dbconn=None):
|
|||||||
|
|
||||||
|
|
||||||
def start_db():
|
def start_db():
|
||||||
|
|
||||||
|
conf.AUX_MODE = True # that is, without a doubt, the worst python code you have ever seen
|
||||||
|
|
||||||
# Upgrade database
|
# Upgrade database
|
||||||
from .. import upgrade
|
from .. import upgrade
|
||||||
upgrade.upgrade_db(sqldb.add_scrobbles)
|
upgrade.upgrade_db(sqldb.add_scrobbles)
|
||||||
@ -941,8 +946,16 @@ def start_db():
|
|||||||
from . import associated
|
from . import associated
|
||||||
associated.load_associated_rules()
|
associated.load_associated_rules()
|
||||||
|
|
||||||
|
# import scrobbles
|
||||||
|
from ..proccontrol.tasks.import_scrobbles import import_scrobbles #lmao this codebase is so fucked
|
||||||
|
for f in os.listdir(data_dir['import']()):
|
||||||
|
if f != 'dummy':
|
||||||
|
import_scrobbles(data_dir['import'](f))
|
||||||
|
|
||||||
dbstatus['healthy'] = True
|
dbstatus['healthy'] = True
|
||||||
|
|
||||||
|
conf.AUX_MODE = False # but you have seen it
|
||||||
|
|
||||||
# inform time module about begin of scrobbling
|
# inform time module about begin of scrobbling
|
||||||
try:
|
try:
|
||||||
firstscrobble = sqldb.get_scrobbles(limit=1)[0]
|
firstscrobble = sqldb.get_scrobbles(limit=1)[0]
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
from datetime import timezone, timedelta, date, time, datetime
|
from datetime import timezone, timedelta, date, time, datetime
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
import math
|
import math
|
||||||
|
import zoneinfo
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from .pkg_global.conf import malojaconfig
|
from .pkg_global.conf import malojaconfig
|
||||||
|
|
||||||
|
|
||||||
OFFSET = malojaconfig["TIMEZONE"]
|
OFFSET = malojaconfig["TIMEZONE"]
|
||||||
TIMEZONE = timezone(timedelta(hours=OFFSET))
|
LOCATION_TIMEZONE = malojaconfig["LOCATION_TIMEZONE"]
|
||||||
|
TIMEZONE = timezone(timedelta(hours=OFFSET)) if not LOCATION_TIMEZONE or LOCATION_TIMEZONE not in zoneinfo.available_timezones() else zoneinfo.ZoneInfo(LOCATION_TIMEZONE)
|
||||||
UTC = timezone.utc
|
UTC = timezone.utc
|
||||||
|
|
||||||
FIRST_SCROBBLE = int(datetime.utcnow().replace(tzinfo=UTC).timestamp())
|
FIRST_SCROBBLE = int(datetime.now(UTC).timestamp())
|
||||||
|
|
||||||
def register_scrobbletime(timestamp):
|
def register_scrobbletime(timestamp):
|
||||||
global FIRST_SCROBBLE
|
global FIRST_SCROBBLE
|
||||||
@ -63,7 +65,7 @@ class MTRangeGeneric(ABC):
|
|||||||
|
|
||||||
# whether we currently live or will ever again live in this range
|
# whether we currently live or will ever again live in this range
|
||||||
def active(self):
|
def active(self):
|
||||||
return (self.last_stamp() > datetime.utcnow().timestamp())
|
return (self.last_stamp() > datetime.now(timezone.utc).timestamp())
|
||||||
|
|
||||||
def __contains__(self,timestamp):
|
def __contains__(self,timestamp):
|
||||||
return timestamp >= self.first_stamp() and timestamp <= self.last_stamp()
|
return timestamp >= self.first_stamp() and timestamp <= self.last_stamp()
|
||||||
@ -111,7 +113,7 @@ class MTRangeGregorian(MTRangeSingular):
|
|||||||
# whether we currently live or will ever again live in this range
|
# whether we currently live or will ever again live in this range
|
||||||
# USE GENERIC SUPER METHOD INSTEAD
|
# USE GENERIC SUPER METHOD INSTEAD
|
||||||
# def active(self):
|
# def active(self):
|
||||||
# tod = datetime.datetime.utcnow().date()
|
# tod = datetime.datetime.now(timezone.utc).date()
|
||||||
# if tod.year > self.year: return False
|
# if tod.year > self.year: return False
|
||||||
# if self.precision == 1: return True
|
# if self.precision == 1: return True
|
||||||
# if tod.year == self.year:
|
# if tod.year == self.year:
|
||||||
@ -328,7 +330,7 @@ class MTRangeComposite(MTRangeGeneric):
|
|||||||
if self.since is None: return FIRST_SCROBBLE
|
if self.since is None: return FIRST_SCROBBLE
|
||||||
else: return self.since.first_stamp()
|
else: return self.since.first_stamp()
|
||||||
def last_stamp(self):
|
def last_stamp(self):
|
||||||
#if self.to is None: return int(datetime.utcnow().replace(tzinfo=timezone.utc).timestamp())
|
#if self.to is None: return int(datetime.now(timezone.utc).timestamp())
|
||||||
if self.to is None: return today().last_stamp()
|
if self.to is None: return today().last_stamp()
|
||||||
else: return self.to.last_stamp()
|
else: return self.to.last_stamp()
|
||||||
|
|
||||||
@ -421,8 +423,8 @@ def get_last_instance(category,current,target,amount):
|
|||||||
|
|
||||||
str_to_time_range = {
|
str_to_time_range = {
|
||||||
**{s:callable for callable,strlist in currenttime_string_representations for s in strlist},
|
**{s:callable for callable,strlist in currenttime_string_representations for s in strlist},
|
||||||
**{s:(lambda i=index:get_last_instance(thismonth,datetime.utcnow().month,i,12)) for index,strlist in enumerate(month_string_representations,1) for s in strlist},
|
**{s:(lambda i=index:get_last_instance(thismonth,datetime.now(timezone.utc).month,i,12)) for index,strlist in enumerate(month_string_representations,1) for s in strlist},
|
||||||
**{s:(lambda i=index:get_last_instance(today,datetime.utcnow().isoweekday()+1%7,i,7)) for index,strlist in enumerate(weekday_string_representations,1) for s in strlist}
|
**{s:(lambda i=index:get_last_instance(today,datetime.now(timezone.utc).isoweekday()+1%7,i,7)) for index,strlist in enumerate(weekday_string_representations,1) for s in strlist}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -207,7 +207,8 @@ malojaconfig = Configuration(
|
|||||||
"filters_remix":(tp.Set(tp.String()), "Remix Filters", ["Remix", "Remix Edit", "Short Mix", "Extended Mix", "Soundtrack Version"], "Filters used to recognize the remix artists in the title"),
|
"filters_remix":(tp.Set(tp.String()), "Remix Filters", ["Remix", "Remix Edit", "Short Mix", "Extended Mix", "Soundtrack Version"], "Filters used to recognize the remix artists in the title"),
|
||||||
"parse_remix_artists":(tp.Boolean(), "Parse Remix Artists", False),
|
"parse_remix_artists":(tp.Boolean(), "Parse Remix Artists", False),
|
||||||
"week_offset":(tp.Integer(), "Week Begin Offset", 0, "Start of the week for the purpose of weekly statistics. 0 = Sunday, 6 = Saturday"),
|
"week_offset":(tp.Integer(), "Week Begin Offset", 0, "Start of the week for the purpose of weekly statistics. 0 = Sunday, 6 = Saturday"),
|
||||||
"timezone":(tp.Integer(), "UTC Offset", 0)
|
"timezone":(tp.Integer(), "UTC Offset", 0),
|
||||||
|
"location_timezone":(tp.String(), "Location Timezone", None)
|
||||||
},
|
},
|
||||||
"Web Interface":{
|
"Web Interface":{
|
||||||
"default_range_startpage":(tp.Choice({'alltime':'All Time','year':'Year','month':"Month",'week':'Week'}), "Default Range for Startpage Stats", "year"),
|
"default_range_startpage":(tp.Choice({'alltime':'All Time','year':'Year','month':"Month",'week':'Week'}), "Default Range for Startpage Stats", "year"),
|
||||||
@ -298,6 +299,7 @@ data_directories = {
|
|||||||
"auth":pthj(dir_settings['state'],"auth"),
|
"auth":pthj(dir_settings['state'],"auth"),
|
||||||
"backups":pthj(dir_settings['state'],"backups"),
|
"backups":pthj(dir_settings['state'],"backups"),
|
||||||
"images":pthj(dir_settings['state'],"images"),
|
"images":pthj(dir_settings['state'],"images"),
|
||||||
|
"import":pthj(dir_settings['state'],"import"),
|
||||||
"scrobbles":pthj(dir_settings['state']),
|
"scrobbles":pthj(dir_settings['state']),
|
||||||
"rules":pthj(dir_settings['config'],"rules"),
|
"rules":pthj(dir_settings['config'],"rules"),
|
||||||
"clients":pthj(dir_settings['config']),
|
"clients":pthj(dir_settings['config']),
|
||||||
|
@ -34,9 +34,12 @@ def import_scrobbles(inputf):
|
|||||||
filename = os.path.basename(inputf)
|
filename = os.path.basename(inputf)
|
||||||
importfunc = None
|
importfunc = None
|
||||||
|
|
||||||
|
if re.match(r"recenttracks-.*\.csv", filename):
|
||||||
|
typeid, typedesc = "lastfm", "Last.fm (ghan CSV)"
|
||||||
|
importfunc = parse_lastfm_ghan_csv
|
||||||
|
|
||||||
if re.match(r".*\.csv", filename):
|
elif re.match(r".*\.csv", filename):
|
||||||
typeid,typedesc = "lastfm", "Last.fm (benjaminbenben export)"
|
typeid,typedesc = "lastfm", "Last.fm (benjaminbenben CSV)"
|
||||||
importfunc = parse_lastfm
|
importfunc = parse_lastfm
|
||||||
|
|
||||||
elif re.match(r"Streaming_History_Audio.+\.json", filename):
|
elif re.match(r"Streaming_History_Audio.+\.json", filename):
|
||||||
@ -65,8 +68,8 @@ def import_scrobbles(inputf):
|
|||||||
importfunc = parse_rockbox
|
importfunc = parse_rockbox
|
||||||
|
|
||||||
elif re.match(r"recenttracks-.*\.json", filename):
|
elif re.match(r"recenttracks-.*\.json", filename):
|
||||||
typeid, typedesc = "lastfm", "Last.fm (ghan export)"
|
typeid, typedesc = "lastfm", "Last.fm (ghan JSON)"
|
||||||
importfunc = parse_lastfm_ghan
|
importfunc = parse_lastfm_ghan_json
|
||||||
|
|
||||||
elif re.match(r".*\.json",filename):
|
elif re.match(r".*\.json",filename):
|
||||||
try:
|
try:
|
||||||
@ -83,8 +86,8 @@ def import_scrobbles(inputf):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
print(f"Parsing {col['yellow'](inputf)} as {col['cyan'](typedesc)} export")
|
print(f"Parsing {col['yellow'](inputf)} as {col['cyan'](typedesc)} export.")
|
||||||
print("This could take a while...")
|
print(col['red']("Please double-check if this is correct - if the import fails, the file might have been interpreted as the wrong type."))
|
||||||
|
|
||||||
timestamps = set()
|
timestamps = set()
|
||||||
scrobblebuffer = []
|
scrobblebuffer = []
|
||||||
@ -154,21 +157,22 @@ def parse_spotify_lite_legacy(inputf):
|
|||||||
inputf = pth.abspath(inputf)
|
inputf = pth.abspath(inputf)
|
||||||
inputfolder = pth.dirname(inputf)
|
inputfolder = pth.dirname(inputf)
|
||||||
filenames = re.compile(r'StreamingHistory[0-9]+\.json')
|
filenames = re.compile(r'StreamingHistory[0-9]+\.json')
|
||||||
inputfiles = [os.path.join(inputfolder,f) for f in os.listdir(inputfolder) if filenames.match(f)]
|
#inputfiles = [os.path.join(inputfolder,f) for f in os.listdir(inputfolder) if filenames.match(f)]
|
||||||
|
inputfiles = [inputf]
|
||||||
|
|
||||||
if len(inputfiles) == 0:
|
#if len(inputfiles) == 0:
|
||||||
print("No files found!")
|
# print("No files found!")
|
||||||
return
|
# return
|
||||||
|
|
||||||
if inputfiles != [inputf]:
|
#if inputfiles != [inputf]:
|
||||||
print("Spotify files should all be imported together to identify duplicates across the whole dataset.")
|
# print("Spotify files should all be imported together to identify duplicates across the whole dataset.")
|
||||||
if not ask("Import " + ", ".join(col['yellow'](pth.basename(i)) for i in inputfiles) + "?",default=True):
|
# if not ask("Import " + ", ".join(col['yellow'](pth.basename(i)) for i in inputfiles) + "?",default=True):
|
||||||
inputfiles = [inputf]
|
# inputfiles = [inputf]
|
||||||
print("Only importing", col['yellow'](pth.basename(inputf)))
|
# print("Only importing", col['yellow'](pth.basename(inputf)))
|
||||||
|
|
||||||
for inputf in inputfiles:
|
for inputf in inputfiles:
|
||||||
|
|
||||||
print("Importing",col['yellow'](inputf),"...")
|
#print("Importing",col['yellow'](inputf),"...")
|
||||||
with open(inputf,'r') as inputfd:
|
with open(inputf,'r') as inputfd:
|
||||||
data = json.load(inputfd)
|
data = json.load(inputfd)
|
||||||
|
|
||||||
@ -207,21 +211,22 @@ def parse_spotify_lite(inputf):
|
|||||||
inputf = pth.abspath(inputf)
|
inputf = pth.abspath(inputf)
|
||||||
inputfolder = pth.dirname(inputf)
|
inputfolder = pth.dirname(inputf)
|
||||||
filenames = re.compile(r'Streaming_History_Audio.+\.json')
|
filenames = re.compile(r'Streaming_History_Audio.+\.json')
|
||||||
inputfiles = [os.path.join(inputfolder,f) for f in os.listdir(inputfolder) if filenames.match(f)]
|
#inputfiles = [os.path.join(inputfolder,f) for f in os.listdir(inputfolder) if filenames.match(f)]
|
||||||
|
inputfiles = [inputf]
|
||||||
|
|
||||||
if len(inputfiles) == 0:
|
#if len(inputfiles) == 0:
|
||||||
print("No files found!")
|
# print("No files found!")
|
||||||
return
|
# return
|
||||||
|
|
||||||
if inputfiles != [inputf]:
|
#if inputfiles != [inputf]:
|
||||||
print("Spotify files should all be imported together to identify duplicates across the whole dataset.")
|
# print("Spotify files should all be imported together to identify duplicates across the whole dataset.")
|
||||||
if not ask("Import " + ", ".join(col['yellow'](pth.basename(i)) for i in inputfiles) + "?",default=True):
|
# if not ask("Import " + ", ".join(col['yellow'](pth.basename(i)) for i in inputfiles) + "?",default=True):
|
||||||
inputfiles = [inputf]
|
# inputfiles = [inputf]
|
||||||
print("Only importing", col['yellow'](pth.basename(inputf)))
|
# print("Only importing", col['yellow'](pth.basename(inputf)))
|
||||||
|
|
||||||
for inputf in inputfiles:
|
for inputf in inputfiles:
|
||||||
|
|
||||||
print("Importing",col['yellow'](inputf),"...")
|
#print("Importing",col['yellow'](inputf),"...")
|
||||||
with open(inputf,'r') as inputfd:
|
with open(inputf,'r') as inputfd:
|
||||||
data = json.load(inputfd)
|
data = json.load(inputfd)
|
||||||
|
|
||||||
@ -267,17 +272,18 @@ def parse_spotify(inputf):
|
|||||||
inputf = pth.abspath(inputf)
|
inputf = pth.abspath(inputf)
|
||||||
inputfolder = pth.dirname(inputf)
|
inputfolder = pth.dirname(inputf)
|
||||||
filenames = re.compile(r'endsong_[0-9]+\.json')
|
filenames = re.compile(r'endsong_[0-9]+\.json')
|
||||||
inputfiles = [os.path.join(inputfolder,f) for f in os.listdir(inputfolder) if filenames.match(f)]
|
#inputfiles = [os.path.join(inputfolder,f) for f in os.listdir(inputfolder) if filenames.match(f)]
|
||||||
|
inputfiles = [inputf]
|
||||||
|
|
||||||
if len(inputfiles) == 0:
|
#if len(inputfiles) == 0:
|
||||||
print("No files found!")
|
# print("No files found!")
|
||||||
return
|
# return
|
||||||
|
|
||||||
if inputfiles != [inputf]:
|
#if inputfiles != [inputf]:
|
||||||
print("Spotify files should all be imported together to identify duplicates across the whole dataset.")
|
# print("Spotify files should all be imported together to identify duplicates across the whole dataset.")
|
||||||
if not ask("Import " + ", ".join(col['yellow'](pth.basename(i)) for i in inputfiles) + "?",default=True):
|
# if not ask("Import " + ", ".join(col['yellow'](pth.basename(i)) for i in inputfiles) + "?",default=True):
|
||||||
inputfiles = [inputf]
|
# inputfiles = [inputf]
|
||||||
print("Only importing", col['yellow'](pth.basename(inputf)))
|
# print("Only importing", col['yellow'](pth.basename(inputf)))
|
||||||
|
|
||||||
# we keep timestamps here as well to remove duplicates because spotify's export
|
# we keep timestamps here as well to remove duplicates because spotify's export
|
||||||
# is messy - this is specific to this import type and should not be mixed with
|
# is messy - this is specific to this import type and should not be mixed with
|
||||||
@ -288,7 +294,7 @@ def parse_spotify(inputf):
|
|||||||
|
|
||||||
for inputf in inputfiles:
|
for inputf in inputfiles:
|
||||||
|
|
||||||
print("Importing",col['yellow'](inputf),"...")
|
#print("Importing",col['yellow'](inputf),"...")
|
||||||
with open(inputf,'r') as inputfd:
|
with open(inputf,'r') as inputfd:
|
||||||
data = json.load(inputfd)
|
data = json.load(inputfd)
|
||||||
|
|
||||||
@ -408,7 +414,7 @@ def parse_lastfm(inputf):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
def parse_lastfm_ghan(inputf):
|
def parse_lastfm_ghan_json(inputf):
|
||||||
with open(inputf, 'r') as inputfd:
|
with open(inputf, 'r') as inputfd:
|
||||||
data = json.load(inputfd)
|
data = json.load(inputfd)
|
||||||
|
|
||||||
@ -430,6 +436,21 @@ def parse_lastfm_ghan(inputf):
|
|||||||
}, '')
|
}, '')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_lastfm_ghan_csv(inputf):
|
||||||
|
with open(inputf, 'r') as inputfd:
|
||||||
|
reader = csv.DictReader(inputfd)
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
yield ('CONFIDENT_IMPORT', {
|
||||||
|
'track_title': row['track'],
|
||||||
|
'track_artists': row['artist'],
|
||||||
|
'track_length': None,
|
||||||
|
'album_name': row['album'],
|
||||||
|
'scrobble_time': int(row['uts']),
|
||||||
|
'scrobble_duration': None
|
||||||
|
}, '')
|
||||||
|
|
||||||
|
|
||||||
def parse_listenbrainz(inputf):
|
def parse_listenbrainz(inputf):
|
||||||
|
|
||||||
with open(inputf,'r') as inputfd:
|
with open(inputf,'r') as inputfd:
|
||||||
|
@ -3,6 +3,7 @@ import os
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
import time
|
import time
|
||||||
|
from magic import from_file
|
||||||
|
|
||||||
|
|
||||||
# server stuff
|
# server stuff
|
||||||
@ -154,7 +155,8 @@ def static_image(pth):
|
|||||||
|
|
||||||
@webserver.route("/cacheimages/<uuid>")
|
@webserver.route("/cacheimages/<uuid>")
|
||||||
def static_proxied_image(uuid):
|
def static_proxied_image(uuid):
|
||||||
return static_file(uuid,root=data_dir['cache']('images'))
|
mimetype = from_file(os.path.join(data_dir['cache']('images'),uuid),True)
|
||||||
|
return static_file(uuid,root=data_dir['cache']('images'),mimetype=mimetype)
|
||||||
|
|
||||||
@webserver.route("/login")
|
@webserver.route("/login")
|
||||||
def login():
|
def login():
|
||||||
@ -165,16 +167,16 @@ def login():
|
|||||||
@webserver.route("/media/<name>.<ext>")
|
@webserver.route("/media/<name>.<ext>")
|
||||||
def static(name,ext):
|
def static(name,ext):
|
||||||
assert ext in ["txt","ico","jpeg","jpg","png","less","js","ttf","css"]
|
assert ext in ["txt","ico","jpeg","jpg","png","less","js","ttf","css"]
|
||||||
with resources.files('maloja') / 'web' / 'static' as staticfolder:
|
staticfolder = resources.files('maloja') / 'web' / 'static'
|
||||||
response = static_file(ext + "/" + name + "." + ext,root=staticfolder)
|
response = static_file(ext + "/" + name + "." + ext,root=staticfolder)
|
||||||
response.set_header("Cache-Control", "public, max-age=3600")
|
response.set_header("Cache-Control", "public, max-age=3600")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# new, direct reference
|
# new, direct reference
|
||||||
@webserver.route("/static/<path:path>")
|
@webserver.route("/static/<path:path>")
|
||||||
def static(path):
|
def static(path):
|
||||||
with resources.files('maloja') / 'web' / 'static' as staticfolder:
|
staticfolder = resources.files('maloja') / 'web' / 'static'
|
||||||
response = static_file(path,root=staticfolder)
|
response = static_file(path,root=staticfolder)
|
||||||
response.set_header("Cache-Control", "public, max-age=3600")
|
response.set_header("Cache-Control", "public, max-age=3600")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
try:
|
from pathlib import PosixPath
|
||||||
from setuptools import distutils
|
|
||||||
except ImportError:
|
|
||||||
import distutils
|
|
||||||
from doreah.io import col, ask, prompt
|
from doreah.io import col, ask, prompt
|
||||||
|
|
||||||
from .pkg_global.conf import data_dir, dir_settings, malojaconfig, auth
|
from .pkg_global.conf import data_dir, dir_settings, malojaconfig, auth
|
||||||
@ -22,21 +22,33 @@ ext_apikeys = [
|
|||||||
|
|
||||||
|
|
||||||
def copy_initial_local_files():
|
def copy_initial_local_files():
|
||||||
with resources.files("maloja") / 'data_files' as folder:
|
data_file_source = resources.files("maloja") / 'data_files'
|
||||||
for cat in dir_settings:
|
for cat in dir_settings:
|
||||||
if dir_settings[cat] is None:
|
if dir_settings[cat] is None:
|
||||||
continue
|
continue
|
||||||
|
if cat == 'config' and malojaconfig.readonly:
|
||||||
|
continue
|
||||||
|
|
||||||
if cat == 'config' and malojaconfig.readonly:
|
# to avoid permission problems with the root dir
|
||||||
continue
|
for subfolder in os.listdir(data_file_source / cat):
|
||||||
|
src = data_file_source / cat / subfolder
|
||||||
|
dst = PosixPath(dir_settings[cat]) / subfolder
|
||||||
|
if os.path.isdir(src):
|
||||||
|
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||||
|
# fix permissions (u+w)
|
||||||
|
for dirpath, _, filenames in os.walk(dst):
|
||||||
|
os.chmod(dirpath, os.stat(dirpath).st_mode | stat.S_IWUSR)
|
||||||
|
for filename in filenames:
|
||||||
|
filepath = os.path.join(dirpath, filename)
|
||||||
|
os.chmod(filepath, os.stat(filepath).st_mode | stat.S_IWUSR)
|
||||||
|
|
||||||
distutils.dir_util.copy_tree(os.path.join(folder,cat),dir_settings[cat],update=False)
|
|
||||||
|
|
||||||
charset = list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
charset = list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
def randomstring(length=32):
|
def randomstring(length=32):
|
||||||
import random
|
import random
|
||||||
return "".join(str(random.choice(charset)) for _ in range(length))
|
return "".join(str(random.choice(charset)) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
def setup():
|
||||||
|
|
||||||
copy_initial_local_files()
|
copy_initial_local_files()
|
||||||
@ -51,10 +63,11 @@ def setup():
|
|||||||
print(f"\tCurrently not using a {col['red'](keyname)} for image display.")
|
print(f"\tCurrently not using a {col['red'](keyname)} for image display.")
|
||||||
elif key is None or key == "ASK":
|
elif key is None or key == "ASK":
|
||||||
if malojaconfig.readonly:
|
if malojaconfig.readonly:
|
||||||
continue
|
print(f"\tCurrently not using a {col['red'](keyname)} for image display - config is read only.")
|
||||||
promptmsg = f"\tPlease enter your {col['gold'](keyname)}. If you do not want to use one at this moment, simply leave this empty and press Enter."
|
else:
|
||||||
key = prompt(promptmsg,types=(str,),default=False,skip=SKIP)
|
promptmsg = f"\tPlease enter your {col['gold'](keyname)}. If you do not want to use one at this moment, simply leave this empty and press Enter."
|
||||||
malojaconfig[k] = key
|
key = prompt(promptmsg,types=(str,),default=False,skip=SKIP)
|
||||||
|
malojaconfig[k] = key
|
||||||
else:
|
else:
|
||||||
print(f"\t{col['lawngreen'](keyname)} found.")
|
print(f"\t{col['lawngreen'](keyname)} found.")
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
|
|
||||||
var xhttp = new XMLHttpRequest();
|
var xhttp = new XMLHttpRequest();
|
||||||
xhttp.open("POST","/api/newrule?", true);
|
xhttp.open("POST","/apis/mlj_1/newrule?", true);
|
||||||
xhttp.send(keys);
|
xhttp.send(keys);
|
||||||
e = arguments[0];
|
e = arguments[0];
|
||||||
line = e.parentNode;
|
line = e.parentNode;
|
||||||
@ -25,7 +25,7 @@
|
|||||||
function fullrebuild() {
|
function fullrebuild() {
|
||||||
|
|
||||||
var xhttp = new XMLHttpRequest();
|
var xhttp = new XMLHttpRequest();
|
||||||
xhttp.open("POST","/api/rebuild", true);
|
xhttp.open("POST","/apis/mlj_1/rebuild", true);
|
||||||
xhttp.send();
|
xhttp.send();
|
||||||
window.location = "/wait";
|
window.location = "/wait";
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
keys = "filename=" + encodeURIComponent(filename);
|
keys = "filename=" + encodeURIComponent(filename);
|
||||||
console.log(keys);
|
console.log(keys);
|
||||||
var xhttp = new XMLHttpRequest();
|
var xhttp = new XMLHttpRequest();
|
||||||
xhttp.open("POST","/api/importrules", true);
|
xhttp.open("POST","/apis/mlj_1/importrules", true);
|
||||||
xhttp.send(keys);
|
xhttp.send(keys);
|
||||||
|
|
||||||
e.innerHTML = e.innerHTML.replace("Add","Remove");
|
e.innerHTML = e.innerHTML.replace("Add","Remove");
|
||||||
@ -36,7 +36,7 @@
|
|||||||
keys = "remove&filename=" + encodeURIComponent(filename);
|
keys = "remove&filename=" + encodeURIComponent(filename);
|
||||||
|
|
||||||
var xhttp = new XMLHttpRequest();
|
var xhttp = new XMLHttpRequest();
|
||||||
xhttp.open("POST","/api/importrules", true);
|
xhttp.open("POST","/apis/mlj_1/importrules", true);
|
||||||
xhttp.send(keys);
|
xhttp.send(keys);
|
||||||
|
|
||||||
e.innerHTML = e.innerHTML.replace("Remove","Add");
|
e.innerHTML = e.innerHTML.replace("Remove","Add");
|
||||||
|
@ -186,7 +186,7 @@ function search_manualscrobbling(searchfield) {
|
|||||||
else {
|
else {
|
||||||
xhttp = new XMLHttpRequest();
|
xhttp = new XMLHttpRequest();
|
||||||
xhttp.onreadystatechange = searchresult_manualscrobbling;
|
xhttp.onreadystatechange = searchresult_manualscrobbling;
|
||||||
xhttp.open("GET","/api/search?max=5&query=" + encodeURIComponent(txt), true);
|
xhttp.open("GET","/apis/mlj_1/search?max=5&query=" + encodeURIComponent(txt), true);
|
||||||
xhttp.send();
|
xhttp.send();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
var searches = []
|
var searches = [];
|
||||||
|
var debounceTimer;
|
||||||
|
|
||||||
function search(searchfield) {
|
function search(searchfield) {
|
||||||
txt = searchfield.value;
|
clearTimeout(debounceTimer);
|
||||||
if (txt == "") {
|
debounceTimer = setTimeout(() => {
|
||||||
reallyclear()
|
const txt = searchfield.value;
|
||||||
}
|
if (txt == "") {
|
||||||
else {
|
reallyclear();
|
||||||
xhttp = new XMLHttpRequest();
|
}
|
||||||
searches.push(xhttp)
|
else {
|
||||||
xhttp.onreadystatechange = searchresult
|
const xhttp = new XMLHttpRequest();
|
||||||
xhttp.open("GET","/api/search?max=5&query=" + encodeURIComponent(txt), true);
|
searches.push(xhttp);
|
||||||
xhttp.send();
|
xhttp.onreadystatechange = searchresult
|
||||||
}
|
xhttp.open("GET","/apis/mlj_1/search?max=5&query=" + encodeURIComponent(txt), true);
|
||||||
|
xhttp.send();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
function upload(encodedentity,b64) {
|
function upload(encodedentity,b64) {
|
||||||
neo.xhttprequest("/api/addpicture?" + encodedentity,{"b64":b64},"POST")
|
neo.xhttprequest("/apis/mlj_1/addpicture?" + encodedentity,{"b64":b64},"POST")
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "malojaserver"
|
name = "malojaserver"
|
||||||
version = "3.2.3"
|
version = "3.2.4"
|
||||||
description = "Self-hosted music scrobble database"
|
description = "Self-hosted music scrobble database"
|
||||||
readme = "./README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = "==3.12.*"
|
||||||
license = { file="./LICENSE" }
|
license = { file="LICENSE" }
|
||||||
authors = [ { name="Johannes Krattenmacher", email="maloja@dev.krateng.ch" } ]
|
authors = [ { name="Johannes Krattenmacher", email="maloja@dev.krateng.ch" } ]
|
||||||
|
|
||||||
urls.repository = "https://github.com/krateng/maloja"
|
urls.repository = "https://github.com/krateng/maloja"
|
||||||
@ -19,33 +19,32 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bottle>=0.12.16",
|
"bottle==0.13.*",
|
||||||
"waitress>=2.1.0",
|
"waitress==3.0.*",
|
||||||
"doreah>=2.0.1, <3",
|
"doreah==2.0.*",
|
||||||
"nimrodel>=0.8.0",
|
"nimrodel==0.8.*",
|
||||||
"setproctitle>=1.1.10",
|
"setproctitle==1.3.*",
|
||||||
#"pyvips>=2.1.16",
|
"jinja2==3.1.*",
|
||||||
"jinja2>=3.0.0",
|
"lru-dict==1.3.*",
|
||||||
"lru-dict>=1.1.6",
|
"psutil==5.9.*",
|
||||||
"psutil>=5.8.0",
|
"sqlalchemy==2.0",
|
||||||
"sqlalchemy>=2.0",
|
"python-datauri==3.0.*",
|
||||||
"python-datauri>=1.1.0",
|
"python-magic==0.4.*",
|
||||||
"requests>=2.27.1",
|
"requests==2.32.*",
|
||||||
"setuptools>68.0.0",
|
"toml==0.10.*",
|
||||||
"toml>=0.10.2",
|
"PyYAML==6.0.*"
|
||||||
"PyYAML>=6.0.1"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
full = [
|
full = [
|
||||||
"pyvips>=2.1"
|
"pyvips==2.2.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
maloja = "maloja.__main__:main"
|
maloja = "maloja.__main__:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["flit_core >=3.2,<4"]
|
requires = ["flit_core >=3.10,<4"]
|
||||||
build-backend = "flit_core.buildapi"
|
build-backend = "flit_core.buildapi"
|
||||||
|
|
||||||
[tool.flit.module]
|
[tool.flit.module]
|
||||||
@ -66,7 +65,8 @@ build =[
|
|||||||
run = [
|
run = [
|
||||||
"python3",
|
"python3",
|
||||||
"py3-lxml",
|
"py3-lxml",
|
||||||
"tzdata"
|
"tzdata",
|
||||||
|
"libmagic"
|
||||||
]
|
]
|
||||||
opt = [
|
opt = [
|
||||||
"vips"
|
"vips"
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
bottle>=0.12.16
|
bottle==0.13.*
|
||||||
waitress>=2.1.0
|
waitress==3.0.*
|
||||||
doreah>=2.0.1, <3
|
doreah==2.0.*
|
||||||
nimrodel>=0.8.0
|
nimrodel==0.8.*
|
||||||
setproctitle>=1.1.10
|
setproctitle==1.3.*
|
||||||
jinja2>=3.0.0
|
jinja2==3.1.*
|
||||||
lru-dict>=1.1.6
|
lru-dict==1.3.*
|
||||||
psutil>=5.8.0
|
psutil==5.9.*
|
||||||
sqlalchemy>=2.0
|
sqlalchemy==2.0
|
||||||
python-datauri>=1.1.0
|
python-datauri==3.0.*
|
||||||
requests>=2.27.1
|
python-magic==0.4.*
|
||||||
setuptools>68.0.0
|
requests==2.32.*
|
||||||
toml>=0.10.2
|
toml==0.10.*
|
||||||
PyYAML>=6.0.1
|
PyYAML==6.0.*
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
pyvips>=2.1
|
pyvips==2.2.*
|
||||||
|
|
||||||
|
@ -68,6 +68,7 @@ Settings File | Environment Variable | Type | Description
|
|||||||
`parse_remix_artists` | `MALOJA_PARSE_REMIX_ARTISTS` | Boolean | Parse Remix Artists
|
`parse_remix_artists` | `MALOJA_PARSE_REMIX_ARTISTS` | Boolean | Parse Remix Artists
|
||||||
`week_offset` | `MALOJA_WEEK_OFFSET` | Integer | Start of the week for the purpose of weekly statistics. 0 = Sunday, 6 = Saturday
|
`week_offset` | `MALOJA_WEEK_OFFSET` | Integer | Start of the week for the purpose of weekly statistics. 0 = Sunday, 6 = Saturday
|
||||||
`timezone` | `MALOJA_TIMEZONE` | Integer | UTC Offset
|
`timezone` | `MALOJA_TIMEZONE` | Integer | UTC Offset
|
||||||
|
`location_timezone` | `MALOJA_LOCATION_TIMEZONE` | String | Location Timezone (overrides `timezone`)
|
||||||
**Web Interface**
|
**Web Interface**
|
||||||
`default_range_startpage` | `MALOJA_DEFAULT_RANGE_STARTPAGE` | Choice | Default Range for Startpage Stats
|
`default_range_startpage` | `MALOJA_DEFAULT_RANGE_STARTPAGE` | Choice | Default Range for Startpage Stats
|
||||||
`default_step_pulse` | `MALOJA_DEFAULT_STEP_PULSE` | Choice | Default Pulse Step
|
`default_step_pulse` | `MALOJA_DEFAULT_STEP_PULSE` | Choice | Default Pulse Step
|
||||||
|
40
setup.py
40
setup.py
@ -1,40 +0,0 @@
|
|||||||
import setuptools
|
|
||||||
import toml
|
|
||||||
|
|
||||||
|
|
||||||
with open("pyproject.toml") as fd:
|
|
||||||
pkgdata = toml.load(fd)
|
|
||||||
projectdata = pkgdata['project']
|
|
||||||
|
|
||||||
|
|
||||||
# extract info
|
|
||||||
with open(projectdata['readme'], "r") as fh:
|
|
||||||
long_description = fh.read()
|
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
name=projectdata['name'],
|
|
||||||
version=projectdata['version'],
|
|
||||||
author=projectdata['authors'][0]['name'],
|
|
||||||
author_email=projectdata['authors'][0]['email'],
|
|
||||||
description=projectdata["description"],
|
|
||||||
license="GPLv3",
|
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type="text/markdown",
|
|
||||||
url=projectdata['urls']['repository'],
|
|
||||||
packages=setuptools.find_packages("."),
|
|
||||||
classifiers=[
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
],
|
|
||||||
python_requires=projectdata['requires-python'],
|
|
||||||
install_requires=projectdata['dependencies'],
|
|
||||||
package_data={'': ['*','*/*','*/*/*','*/*/*/*','*/*/.*','*/*/*/.*']},
|
|
||||||
include_package_data=True,
|
|
||||||
entry_points = {
|
|
||||||
'console_scripts':[
|
|
||||||
k + '=' + projectdata['scripts'][k] for k in projectdata['scripts']
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
Loading…
x
Reference in New Issue
Block a user