├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── auxil.library.pypi.yml │ ├── auxil.scrobbler.upload.yml │ ├── docker.yml │ └── pypi.yml ├── .gitignore ├── API.md ├── Containerfile ├── DEVELOPMENT.md ├── FUTURE.md ├── LICENSE ├── README.md ├── auxiliary ├── chromium_scrobbler │ ├── .gitignore │ ├── maloja-scrobbler.zip │ └── maloja-scrobbler │ │ ├── background.js │ │ ├── icon128.png │ │ ├── icon256.png │ │ ├── icon48.png │ │ ├── manifest.json │ │ ├── settings.html │ │ ├── settings.js │ │ ├── sites │ │ ├── bandcamp.js │ │ ├── navidrome.js │ │ ├── plex.js │ │ ├── soundcloud.js │ │ ├── spotify.js │ │ └── ytmusic.js │ │ └── sitescript.js └── malojalib │ ├── README.md │ ├── malojalib │ └── __init__.py │ └── pyproject.toml ├── container └── root │ └── etc │ └── s6-overlay │ └── s6-rc.d │ ├── init-permission-check │ ├── dependencies.d │ │ └── init-config │ ├── run │ ├── type │ └── up │ ├── svc-python │ ├── dependencies.d │ │ └── init-services │ ├── run │ └── type │ └── user │ └── contents.d │ ├── init-permission-check │ └── svc-python ├── dev ├── clear_testdata.sh ├── docker-compose.yml ├── list_tags.sh ├── releases │ ├── 1.0.yml │ ├── 1.1.yml │ ├── 1.2.yml │ ├── 1.3.yml │ ├── 1.4.yml │ ├── 1.5.yml │ ├── 2.0.yml │ ├── 2.1.yml │ ├── 2.10.yml │ ├── 2.11.yml │ ├── 2.12.yml │ ├── 2.13.yml │ ├── 2.14.yml │ ├── 2.2.yml │ ├── 2.3.yml │ ├── 2.4.yml │ ├── 2.5.yml │ ├── 2.6.yml │ ├── 2.7.yml │ ├── 2.8.yml │ ├── 2.9.yml │ ├── 3.0.yml │ ├── 3.1.yml │ └── 3.2.yml ├── templates │ ├── requirements.txt.jinja │ └── requirements_extra.txt.jinja ├── testing │ ├── Maloja.postman_collection.json │ └── stresstest.py ├── update_dist_files.py ├── update_scrobbler.sh └── write_tags.py ├── example-compose.yml ├── maloja ├── __init__.py ├── __main__.py ├── __pkginfo__.py ├── apis │ ├── __init__.py │ ├── _apikeys.py │ ├── _base.py │ ├── _exceptions.py │ ├── audioscrobbler.py │ ├── audioscrobbler_legacy.py │ ├── listenbrainz.py │ └── native_v1.py ├── cleanup.py ├── data_files │ ├── cache │ │ ├── .maloja_cache_sentinel │ │ └── images │ │ │ └── dummy │ ├── config │ │ ├── .maloja_config_sentinel │ │ ├── custom_css │ │ │ └── customcss.info │ │ └── rules │ │ │ ├── predefined │ │ │ ├── .gitignore │ │ │ ├── krateng_artistsingroups.tsv │ │ │ ├── krateng_classical.tsv │ │ │ ├── krateng_cpop.tsv │ │ │ ├── krateng_dach.tsv │ │ │ ├── krateng_firefly-soundtrack.tsv │ │ │ ├── krateng_jeremysoule.tsv │ │ │ ├── krateng_jpop.tsv │ │ │ ├── krateng_kpop.tsv │ │ │ ├── krateng_kpopgirlgroups.tsv │ │ │ ├── krateng_lotr-soundtrack.tsv │ │ │ ├── krateng_masseffect.tsv │ │ │ ├── krateng_memes.tsv │ │ │ ├── krateng_monstercat.tsv │ │ │ ├── krateng_redcliff.tsv │ │ │ ├── krateng_specialsymbols.tsv │ │ │ ├── krateng_threelions.tsv │ │ │ ├── predefined.info │ │ │ └── tdemin_kpop.tsv │ │ │ └── rules.info │ ├── logs │ │ ├── .maloja_logs_sentinel │ │ └── dbfix │ │ │ └── dummy │ └── state │ │ ├── .maloja_state_sentinel │ │ ├── auth │ │ └── dummy │ │ ├── backups │ │ └── dummy │ │ ├── images │ │ ├── albums │ │ │ └── dummy │ │ ├── artists │ │ │ └── dummy │ │ ├── images.info │ │ └── tracks │ │ │ └── dummy │ │ └── import │ │ └── dummy ├── database │ ├── __init__.py │ ├── associated.py │ ├── dbcache.py │ ├── exceptions.py │ ├── jinjaview.py │ └── sqldb.py ├── dev │ ├── __init__.py │ ├── apidebug.py │ ├── generate.py │ └── profiler.py ├── images.py ├── jinjaenv │ ├── context.py │ └── filters.py ├── malojatime.py ├── malojauri.py ├── pkg_global │ ├── conf.py │ └── monkey.py ├── proccontrol │ ├── __init__.py │ └── tasks │ │ ├── __init__.py │ │ ├── backup.py │ │ ├── export.py │ │ ├── import_scrobbles.py │ │ └── parse_albums.py ├── server.py ├── setup.py ├── thirdparty │ ├── __init__.py │ ├── audiodb.py │ ├── deezer.py │ ├── lastfm.py │ ├── maloja.py │ ├── musicbrainz.py │ └── spotify.py ├── upgrade.py └── web │ ├── jinja │ ├── about.jinja │ ├── abstracts │ │ ├── admin.jinja │ │ └── base.jinja │ ├── admin_albumless.jinja │ ├── admin_apikeys.jinja │ ├── admin_import.jinja │ ├── admin_issues.jinja │ ├── admin_manual.jinja │ ├── admin_overview.jinja │ ├── admin_settings.jinja │ ├── admin_setup.jinja │ ├── album.jinja │ ├── artist.jinja │ ├── charts_albums.jinja │ ├── charts_artists.jinja │ ├── charts_tracks.jinja │ ├── error.jinja │ ├── icons │ │ ├── LICENSE-material │ │ ├── LICENSE-octicons │ │ ├── add_album.jinja │ │ ├── add_album_confirm.jinja │ │ ├── add_artist.jinja │ │ ├── add_artist_confirm.jinja │ │ ├── association_cancel.jinja │ │ ├── association_mark.jinja │ │ ├── association_unmark.jinja │ │ ├── cert_album.jinja │ │ ├── cert_track.jinja │ │ ├── delete.jinja │ │ ├── disassociate.jinja │ │ ├── edit.jinja │ │ ├── merge.jinja │ │ ├── merge_cancel.jinja │ │ ├── merge_mark.jinja │ │ ├── merge_unmark.jinja │ │ ├── nodata.jinja │ │ ├── remove_album.jinja │ │ ├── remove_artist.jinja │ │ ├── reparse.jinja │ │ └── settings.jinja │ ├── partials │ │ ├── album_showcase.jinja │ │ ├── awards_album.jinja │ │ ├── awards_artist.jinja │ │ ├── awards_track.jinja │ │ ├── charts_albums.jinja │ │ ├── charts_albums_tiles.jinja │ │ ├── charts_artists.jinja │ │ ├── charts_artists_tiles.jinja │ │ ├── charts_tracks.jinja │ │ ├── charts_tracks_tiles.jinja │ │ ├── info_album.jinja │ │ ├── info_artist.jinja │ │ ├── info_track.jinja │ │ ├── list_tracks.jinja │ │ ├── performance.jinja │ │ ├── pulse.jinja │ │ ├── scrobbles.jinja │ │ ├── top_albums.jinja │ │ ├── top_artists.jinja │ │ └── top_tracks.jinja │ ├── performance.jinja │ ├── pulse.jinja │ ├── scrobbles.jinja │ ├── snippets │ │ ├── entityrow.jinja │ │ ├── filterdescription.jinja │ │ ├── links.jinja │ │ ├── pagination.jinja │ │ └── timeselection.jinja │ ├── start.jinja │ ├── startpage_modules │ │ ├── charts_albums.jinja │ │ ├── charts_artists.jinja │ │ ├── charts_tracks.jinja │ │ ├── featured.jinja │ │ ├── lastscrobbles.jinja │ │ └── pulse.jinja │ ├── top_albums.jinja │ ├── top_artists.jinja │ ├── top_tracks.jinja │ ├── track.jinja │ └── wait.jinja │ └── static │ ├── css │ ├── grisons.css │ ├── grisonsfont.css │ ├── maloja.css │ ├── startpage.css │ └── themes │ │ ├── constantinople.css │ │ ├── kda.css │ │ └── maloja.css │ ├── ico │ ├── favicon.ico │ └── favicon_old.ico │ ├── js │ ├── datechange.js │ ├── edit.js │ ├── lazyload17-8-2.min.js │ ├── manualscrobble.js │ ├── neopolitan.js │ ├── notifications.js │ ├── search.js │ ├── statselect.js │ └── upload.js │ ├── png │ ├── chartpos_bronze.png │ ├── chartpos_gold.png │ ├── chartpos_normal.png │ ├── chartpos_silver.png │ ├── favicon.png │ ├── favicon_large.png │ ├── favicon_old.png │ ├── star.png │ └── star_alt.png │ ├── svg │ ├── LICENSE │ ├── placeholder_album.svg │ ├── placeholder_artist.svg │ └── placeholder_track.svg │ ├── ttf │ ├── Ubuntu-Bold.ttf │ ├── Ubuntu-BoldItalic.ttf │ ├── Ubuntu-Italic.ttf │ ├── Ubuntu-Light.ttf │ ├── Ubuntu-LightItalic.ttf │ ├── Ubuntu-Medium.ttf │ ├── Ubuntu-MediumItalic.ttf │ └── Ubuntu-Regular.ttf │ └── txt │ └── robots.txt ├── pylintrc ├── pyproject.toml ├── requirements.txt ├── requirements_extra.txt ├── screenshot.png └── settings.md /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !maloja 3 | !container 4 | !Containerfile 5 | !requirements.txt 6 | !pyproject.toml 7 | !README.md 8 | !LICENSE 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://paypal.me/krateng"] 2 | patreon: krateng 3 | -------------------------------------------------------------------------------- /.github/workflows/auxil.library.pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish library to PyPI 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'auxiliary/malojalib/pyproject.toml' 7 | # When the version updates, this file changes 8 | # False positives only result in a failed push 9 | 10 | jobs: 11 | publish_to_pypi: 12 | name: Push Library to PyPI 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out the repo 16 | uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a 20 | with: 21 | python-version: '3.x' 22 | 23 | - name: Install dependencies 24 | run: pip install build 25 | 26 | - name: Change directory 27 | run: cd auxiliary/malojalib 28 | 29 | - name: Build package 30 | run: python -m build 31 | 32 | - name: Publish to PyPI 33 | uses: pypa/gh-action-pypi-publish@717ba43cfbb0387f6ce311b169a825772f54d295 34 | with: 35 | user: __token__ 36 | password: ${{ secrets.PYPI_API_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/auxil.scrobbler.upload.yml: -------------------------------------------------------------------------------- 1 | name: Publish Chromium scrobbler to web store 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'auxiliary/chromium_scrobbler/maloja-scobbler/manifest.json' 7 | # When the version updates, this file changes 8 | 9 | jobs: 10 | publish_to_pypi: 11 | name: Build and publish extension 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Push Extension to Web Store 15 | uses: Klemensas/chrome-extension-upload-action@1e8ede84548583abf1a2a495f4242c4c51539337 16 | with: 17 | refresh-token: '${{ secrets.GOOGLE_REFRESHTOKEN }}' 18 | client-id: '${{ secrets.GOOGLE_CLIENTID }}' 19 | file-name: './auxiliary/chromium_scrobbler/maloja-scrobbler.zip' 20 | app-id: 'cfnbifdmgbnaalphodcbandoopgbfeeh' 21 | publish: true 22 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build and release docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | - 'runaction-docker' 8 | 9 | jobs: 10 | push_to_registry: 11 | name: Push Docker image to Docker Hub 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out the repo 15 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 16 | 17 | - name: Log in to Docker Hub 18 | if: github.event_name != 'pull_request' 19 | uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} 23 | 24 | - name: Extract metadata (tags, labels) for Docker 25 | id: meta 26 | uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 27 | with: 28 | images: | 29 | ${{ github.repository_owner }}/maloja 30 | # generate Docker tags based on the following events/attributes 31 | tags: | 32 | type=semver,pattern={{version}} 33 | flavor: | 34 | latest=true 35 | 36 | - name: Set up QEMU 37 | uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 38 | 39 | - name: Set up Docker Buildx 40 | uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 41 | 42 | - name: Cache Docker layers 43 | uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 44 | with: 45 | path: /tmp/.buildx-cache 46 | key: ${{ runner.os }}-buildx-${{ github.sha }} 47 | restore-keys: | 48 | ${{ runner.os }}-buildx- 49 | 50 | - name: Build and push Docker image 51 | uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 52 | with: 53 | context: . 54 | file: Containerfile 55 | push: ${{ github.event_name != 'pull_request' }} 56 | tags: ${{ steps.meta.outputs.tags }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | platforms: linux/amd64,linux/arm64 #,linux/arm/v7 #build this ourselves GH: #229 59 | cache-from: type=local,src=/tmp/.buildx-cache 60 | cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max 61 | 62 | # Temp fix 63 | # https://github.com/docker/build-push-action/issues/252 64 | # https://github.com/moby/buildkit/issues/1896 65 | - name: Move cache 66 | run: | 67 | rm -rf /tmp/.buildx-cache 68 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 69 | 70 | - name: Update Readme and short description 71 | uses: peter-evans/dockerhub-description@dc67fad7001ef9e8e3c124cb7a64e16d0a63d864 72 | continue-on-error: true 73 | with: 74 | username: ${{ secrets.DOCKERHUB_USERNAME }} 75 | password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} 76 | repository: krateng/maloja 77 | short-description: ${{ github.event.repository.description }} 78 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | - 'runaction-pypi' 8 | 9 | jobs: 10 | publish_to_pypi: 11 | name: Push Package to PyPI 12 | runs-on: ubuntu-latest 13 | permissions: 14 | id-token: write 15 | steps: 16 | - name: Check out the repo 17 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 21 | with: 22 | python-version: '3.x' 23 | 24 | - name: Install dependencies 25 | run: pip install build 26 | 27 | - name: Build package 28 | run: python -m build 29 | 30 | - name: Publish to PyPI 31 | uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # temporary / generated files 2 | *.pyc 3 | 4 | # environments / builds 5 | .venv/* 6 | /dist 7 | /build 8 | /*.egg-info 9 | 10 | # dev files 11 | *.xcf 12 | *.note 13 | *-old 14 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM lsiobase/alpine:3.21 AS base 2 | 3 | WORKDIR /usr/src/app 4 | 5 | 6 | 7 | COPY --chown=abc:abc ./requirements.txt ./requirements.txt 8 | 9 | # based on https://github.com/linuxserver/docker-pyload-ng/blob/main/Dockerfile 10 | # everything but the app installation is run in one command so we can purge 11 | # all build dependencies and cache in the same layer 12 | # it may be possible to decrease image size slightly by using build stage and 13 | # copying all site-packages to runtime stage but the image is already pretty small 14 | RUN \ 15 | echo "" && \ 16 | echo "**** install build packages ****" && \ 17 | apk add --no-cache --virtual=build-deps \ 18 | gcc \ 19 | g++ \ 20 | python3-dev \ 21 | libxml2-dev \ 22 | libxslt-dev \ 23 | libffi-dev \ 24 | libc-dev \ 25 | py3-pip \ 26 | linux-headers && \ 27 | echo "" && \ 28 | echo "**** install runtime packages ****" && \ 29 | apk add --no-cache \ 30 | python3 \ 31 | py3-lxml \ 32 | libmagic \ 33 | tzdata && \ 34 | echo "" && \ 35 | echo "**** install pip dependencies ****" && \ 36 | python3 -m venv /venv && \ 37 | . /venv/bin/activate && \ 38 | python3 -m ensurepip && \ 39 | pip install -U --no-cache-dir \ 40 | pip \ 41 | wheel && \ 42 | echo "" && \ 43 | echo "**** install maloja requirements ****" && \ 44 | pip install --no-cache-dir -r requirements.txt && \ 45 | echo "" && \ 46 | echo "**** cleanup ****" && \ 47 | apk del --purge \ 48 | build-deps && \ 49 | rm -rf \ 50 | /tmp/* \ 51 | ${HOME}/.cache 52 | 53 | # actual installation in extra layer so we can cache the stuff above 54 | 55 | COPY --chown=abc:abc . . 56 | 57 | RUN \ 58 | echo "" && \ 59 | echo "**** install maloja ****" && \ 60 | apk add --no-cache --virtual=install-deps \ 61 | py3-pip && \ 62 | python3 -m venv /venv && \ 63 | . /venv/bin/activate && \ 64 | pip3 install /usr/src/app && \ 65 | apk del --purge \ 66 | install-deps && \ 67 | rm -rf \ 68 | /tmp/* \ 69 | ${HOME}/.cache 70 | 71 | 72 | 73 | COPY container/root/ / 74 | 75 | ENV \ 76 | # Docker-specific configuration 77 | MALOJA_SKIP_SETUP=yes \ 78 | MALOJA_CONTAINER=yes \ 79 | PYTHONUNBUFFERED=1 \ 80 | # Prevents breaking change for previous container that ran maloja as root 81 | # On linux hosts (non-podman rootless) these variables should be set to the 82 | # host user that should own the host folder bound to MALOJA_DATA_DIRECTORY 83 | PUID=0 \ 84 | PGID=0 85 | 86 | EXPOSE 42010 87 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Clone the repository and enter it. 4 | 5 | ```console 6 | git clone https://github.com/krateng/maloja 7 | cd maloja 8 | ``` 9 | 10 | ## Environment 11 | 12 | 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. 13 | 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`. 14 | 15 | 16 | ## Running the server 17 | 18 | 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. 19 | 20 | 21 | ## Further help 22 | 23 | Feel free to [ask](https://github.com/krateng/maloja/discussions) if you need some help! 24 | -------------------------------------------------------------------------------- /FUTURE.md: -------------------------------------------------------------------------------- 1 | Если вы обнаружили этот репозиторий как часть GitHub Arctic Code Vault, я хотел бы искренне извиниться за то, что вы посвятили свой взгляд этому ужасному коду. 2 | 3 | 如果您已将该存储库作为GitHub Arctic Code Vault的一部分发现,我谨诚挚地道歉,因为您将目光投向了这一可怕的代码。 4 | 5 | Hoc si repositum partem GitHub arcticum Codicis Buy nudatus, subdens excusare velim simpliciter gravissimum intueri codice. 6 | 7 | Εάν έχετε αποκαλύψει αυτό το αποθετήριο ως μέρος του GitHub Arctic Code Vault, θα ήθελα ειλικρινά να ζητήσω συγνώμη για την υποβολή των ματιών σας σε αυτόν τον τρομερό κώδικα. 8 | 9 | Եթե դուք հայտնաբերել եք այս պահեստը որպես GitHub Arctic Code Vault- ի մաս, ապա ես կցանկանայի անկեղծորեն ներողություն խնդրել ձեր աչքերը այս սարսափելի կոդին ենթարկելու համար: 10 | 11 | If you have uncovered this repository as part of the GitHub Arctic Code Vault, I would like to sincerely apologize for subjecting your eyes to this terrible code. 12 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/.gitignore: -------------------------------------------------------------------------------- 1 | screenshot.png 2 | tile.png 3 | !*.sh 4 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/auxiliary/chromium_scrobbler/maloja-scrobbler.zip -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/auxiliary/chromium_scrobbler/maloja-scrobbler/icon128.png -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/auxiliary/chromium_scrobbler/maloja-scrobbler/icon256.png -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/auxiliary/chromium_scrobbler/maloja-scrobbler/icon48.png -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Maloja Scrobbler", 3 | "version": "1.13", 4 | "description": "Scrobbles tracks from various sites to your Maloja server", 5 | "manifest_version": 2, 6 | "permissions": [ 7 | "tabs", 8 | "storage", 9 | "http://*/", 10 | "https://*/" 11 | ], 12 | "background": 13 | { 14 | "scripts": 15 | [ 16 | "background.js" 17 | ] 18 | }, 19 | 20 | 21 | "browser_action": 22 | { 23 | "default_icon": 24 | { 25 | "128":"icon128.png", 26 | "48":"icon48.png" 27 | }, 28 | "default_popup": "settings.html", 29 | "default_title": "Maloja Scrobbler" 30 | }, 31 | "icons": 32 | { 33 | "128":"icon128.png", 34 | "48":"icon48.png" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wat 6 | 7 | 8 | 28 | 29 | 30 |
31 | Server:
32 | 33 |

34 | API key:
35 | 36 |
37 | Tabs: 38 | 39 | 40 |
41 | Services: 42 | 43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/sites/bandcamp.js: -------------------------------------------------------------------------------- 1 | maloja_scrobbler_selector_playbar = "//div[contains(@class,'trackView')]" 2 | 3 | 4 | maloja_scrobbler_selector_metadata = "." 5 | // need to select everything as bar / metadata block because artist isn't shown in the inline player 6 | 7 | maloja_scrobbler_selector_title = ".//span[@class='title']/text()" 8 | maloja_scrobbler_selector_artist = ".//span[contains(@itemprop,'byArtist')]/a/text()" 9 | maloja_scrobbler_selector_duration = ".//span[@class='time_total']/text()" 10 | 11 | 12 | maloja_scrobbler_selector_control = ".//td[@class='play_cell']/a[@role='button']/div[contains(@class,'playbutton')]/@class" 13 | 14 | maloja_scrobbler_label_playing = "playbutton playing" 15 | maloja_scrobbler_label_paused = "playbutton" 16 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/sites/navidrome.js: -------------------------------------------------------------------------------- 1 | maloja_scrobbler_selector_playbar = "//div[contains(@class,'music-player-panel')]" 2 | 3 | 4 | maloja_scrobbler_selector_metadata = ".//span[contains(@class,'audio-title')]" 5 | 6 | maloja_scrobbler_selector_title = ".//span[contains(@class,'songTitle')]/text()" 7 | maloja_scrobbler_selector_artist = ".//span[contains(@class,'songArtist')]/text()" 8 | maloja_scrobbler_selector_duration = ".//span[contains(@class,'duration')]/text()" 9 | 10 | 11 | maloja_scrobbler_selector_control = ".//span[contains(@class,'group play-btn')]/@title" 12 | 13 | maloja_scrobbler_label_playing = "Click to pause" 14 | maloja_scrobbler_label_paused = "Click to play" 15 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/sites/plex.js: -------------------------------------------------------------------------------- 1 | maloja_scrobbler_selector_playbar = "//div[contains(@class,'PlayerControls')]" 2 | 3 | 4 | maloja_scrobbler_selector_metadata = ".//div[contains(@class,'PlayerControlsMetadata-container')]" 5 | 6 | maloja_scrobbler_selector_title = ".//a[@data-testid='metadataTitleLink']/@title" 7 | maloja_scrobbler_selector_artist = ".//span[contains(@class,'MetadataPosterTitle-title')]/a[1]/@title" 8 | maloja_scrobbler_selector_duration = ".//button[@data-testid='mediaDuration']/text()[3]" 9 | 10 | 11 | maloja_scrobbler_selector_control = ".//div[contains(@class,'PlayerControls-buttonGroupCenter')]/button[2]/@title" 12 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/sites/soundcloud.js: -------------------------------------------------------------------------------- 1 | maloja_scrobbler_selector_playbar = "//div[contains(@class,'playControls')]" 2 | 3 | 4 | maloja_scrobbler_selector_metadata = ".//div[contains(@class,'playControls__soundBadge')]//div[contains(@class,'playbackSoundBadge__titleContextContainer')]" 5 | 6 | maloja_scrobbler_selector_title = ".//div/a/@title" 7 | maloja_scrobbler_selector_artist = ".//a/text()" 8 | maloja_scrobbler_selector_duration = ".//div[contains(@class,'playbackTimeline__duration')]//span[@aria-hidden='true']/text()" 9 | 10 | 11 | maloja_scrobbler_selector_control = ".//button[contains(@class,'playControl')]/@title" 12 | 13 | maloja_scrobbler_label_playing = "Pause current" 14 | maloja_scrobbler_label_paused = "Play current" 15 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/sites/spotify.js: -------------------------------------------------------------------------------- 1 | maloja_scrobbler_selector_playbar = "//footer[@data-testid='now-playing-bar']" 2 | 3 | 4 | maloja_scrobbler_selector_metadata = ".//div[@data-testid='now-playing-widget']" 5 | 6 | maloja_scrobbler_selector_title = ".//a[@data-testid='context-item-link']/text()" 7 | maloja_scrobbler_selector_artists = ".//a[contains(@href,'/artist/')]" 8 | maloja_scrobbler_selector_artist = "./text()" 9 | maloja_scrobbler_selector_duration = ".//div[@data-testid='playback-duration']/text()" 10 | 11 | 12 | maloja_scrobbler_selector_control = ".//button[@data-testid='control-button-playpause']/@aria-label" 13 | -------------------------------------------------------------------------------- /auxiliary/chromium_scrobbler/maloja-scrobbler/sites/ytmusic.js: -------------------------------------------------------------------------------- 1 | maloja_scrobbler_selector_playbar = "//ytmusic-player-bar" 2 | 3 | 4 | maloja_scrobbler_selector_metadata = ".//div[contains(@class,'middle-controls')]/div[contains(@class,'content-info-wrapper')]" 5 | 6 | maloja_scrobbler_selector_title = ".//yt-formatted-string[contains(@class,'title')]/@title" 7 | maloja_scrobbler_selector_artists = ".//span/span[contains(@class,'subtitle')]/yt-formatted-string/a[position() x.xpath(maloja_scrobbler_selector_artist, XPathResult.STRING_TYPE)); 48 | artist = artists.join(";"); 49 | } 50 | else { 51 | artist = metadata.xpath(maloja_scrobbler_selector_artist, XPathResult.STRING_TYPE); 52 | } 53 | 54 | 55 | if (typeof duration_needs_split !== "undefined" && duration_needs_split) { 56 | duration = duration.split("/").slice(-1)[0].trim(); 57 | } 58 | 59 | if (duration.split(":").length == 2) { 60 | durationSeconds = parseInt(duration.split(":")[0]) * 60 + parseInt(duration.split(":")[1]); 61 | } 62 | else { 63 | durationSeconds = parseInt(duration.split(":")[0]) * 60 * 60 + parseInt(duration.split(":")[1]) * 60 + parseInt(duration.split(":")[2]); 64 | } 65 | 66 | 67 | control = bar.xpath(maloja_scrobbler_selector_control, XPathResult.STRING_TYPE); 68 | try { 69 | label_playing = maloja_scrobbler_label_playing 70 | } 71 | catch { 72 | label_playing = "Pause" 73 | } 74 | try { 75 | label_paused = maloja_scrobbler_label_paused 76 | } 77 | catch { 78 | label_paused = "Play" 79 | } 80 | if (control == label_paused) { 81 | console.log("[Maloja Scrobbler] Not playing right now"); 82 | chrome.runtime.sendMessage({type:"stopPlayback",time:Math.floor(Date.now()),artist:artist,title:title}); 83 | //stopPlayback() 84 | } 85 | else if (control == label_playing) { 86 | console.log("[Maloja Scrobbler] Playing " + artist + " - " + title + " (" + durationSeconds + " sec)"); 87 | chrome.runtime.sendMessage({type:"startPlayback",time:Math.floor(Date.now()),artist:artist,title:title,duration:durationSeconds}); 88 | //startPlayback(artist,title,durationSeconds) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /auxiliary/malojalib/README.md: -------------------------------------------------------------------------------- 1 | # maloja-lib 2 | 3 | Library for Python music players to allow users to scrobble to [Maloja](https://github.com/krateng/maloja) servers. 4 | 5 | ``` 6 | from malojalib import MalojaInstance 7 | 8 | instance = MalojaInstance(user_supplied_url,user_supplied_key) 9 | 10 | instance.scrobble(artists=['K/DA','Howard Shore','Blackbeard's Tea Party],title='Grüezi Wohl Frau Stirnimaa') 11 | 12 | ``` 13 | -------------------------------------------------------------------------------- /auxiliary/malojalib/malojalib/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | class MalojaInstance: 4 | def __init__(self,base_url,key): 5 | self.base_url = base_url 6 | self.key = key 7 | 8 | def test(self): 9 | url = self.base_url + '/apis/mlj_1/test' 10 | response = requests.get(url,{'key':self.key}) 11 | 12 | return (response.status_code == 200) 13 | 14 | def scrobble(self,artists,title,timestamp=None,album=None,duration=None): 15 | payload = { 16 | 'key':self.key, 17 | 'artists':artists, 18 | 'title':title, 19 | 'time':timestamp, 20 | 'album':album, 21 | 'duration':duration 22 | } 23 | 24 | url = self.base_url + '/apis/mlj_1/newscrobble' 25 | response = requests.post(url,payload) 26 | 27 | return response.json() 28 | -------------------------------------------------------------------------------- /auxiliary/malojalib/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "maloja-lib" 3 | version = "1.0.0" 4 | description = "Utilities to interact with Maloja servers" 5 | readme = "./README.md" 6 | requires-python = ">=3.6" 7 | license = { file="../../LICENSE" } 8 | authors = [ { name="Johannes Krattenmacher", email="maloja@dev.krateng.ch" } ] 9 | 10 | urls.repository = "https://github.com/krateng/maloja" 11 | urls.documentation = "https://github.com/krateng/maloja" 12 | 13 | keywords = ["scrobbling", "music", "library", "api"] 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 17 | "Operating System :: OS Independent" 18 | ] 19 | 20 | dependencies = [ 21 | "requests" 22 | ] 23 | 24 | 25 | [build-system] 26 | requires = ["flit_core >=3.2,<4"] 27 | build-backend = "flit_core.buildapi" 28 | -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/init-permission-check/dependencies.d/init-config: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/container/root/etc/s6-overlay/s6-rc.d/init-permission-check/dependencies.d/init-config -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/init-permission-check/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | if [ "$(s6-setuidgid abc id -u)" = "0" ]; then 4 | echo "-------------------------------------" 5 | echo "WARN: Running as root! If you meant to do this than this message can be ignored." 6 | echo "If you are running this container on a *linux* host and are not using podman rootless you SHOULD" 7 | echo "change the ENVs PUID and PGID for this container to ensure correct permissions on your config folder." 8 | echo -e "See: https://github.com/krateng/maloja#linux-host\n" 9 | echo -e "-------------------------------------\n" 10 | fi -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/init-permission-check/type: -------------------------------------------------------------------------------- 1 | oneshot 2 | -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/init-permission-check/up: -------------------------------------------------------------------------------- 1 | /etc/s6-overlay/s6-rc.d/init-permission-check/run 2 | -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/svc-python/dependencies.d/init-services: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/container/root/etc/s6-overlay/s6-rc.d/svc-python/dependencies.d/init-services -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/svc-python/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | # used https://github.com/linuxserver/docker-wikijs/blob/master/root/etc/s6-overlay/s6-rc.d/svc-wikijs/run as a template 4 | 5 | echo -e "\nMaloja is starting!" 6 | exec \ 7 | s6-setuidgid abc /venv/bin/python -m maloja run -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/svc-python/type: -------------------------------------------------------------------------------- 1 | longrun 2 | -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/user/contents.d/init-permission-check: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/container/root/etc/s6-overlay/s6-rc.d/user/contents.d/init-permission-check -------------------------------------------------------------------------------- /container/root/etc/s6-overlay/s6-rc.d/user/contents.d/svc-python: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/container/root/etc/s6-overlay/s6-rc.d/user/contents.d/svc-python -------------------------------------------------------------------------------- /dev/clear_testdata.sh: -------------------------------------------------------------------------------- 1 | sudo rm -r ./testdata 2 | mkdir ./testdata 3 | chmod 777 ./testdata -------------------------------------------------------------------------------- /dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | maloja: 3 | build: 4 | context: .. 5 | dockerfile: ./Containerfile 6 | ports: 7 | - "42010:42010" 8 | volumes: 9 | - "./testdata:/data" 10 | environment: 11 | - "MALOJA_DATA_DIRECTORY=/data" 12 | - "PUID=1000" 13 | - "PGID=1000" 14 | -------------------------------------------------------------------------------- /dev/list_tags.sh: -------------------------------------------------------------------------------- 1 | git tag -l '*.0' -n1 --sort=v:refname 2 | -------------------------------------------------------------------------------- /dev/releases/1.0.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Yura" 2 | '1.0': 3 | commit: "1fac2ca965fdbe40c85a88559d5b736f4829e7b0" 4 | -------------------------------------------------------------------------------- /dev/releases/1.1.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Solar" 2 | '1.1': 3 | commit: "5603ca9eb137516e604e9e3e83e273a70ef32f65" 4 | -------------------------------------------------------------------------------- /dev/releases/1.2.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Jeonghwa" 2 | '1.2': 3 | commit: "d46d2be2bf27ef40ddd9f0c077f86dcf0214adbb" 4 | -------------------------------------------------------------------------------- /dev/releases/1.3.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "IU" 2 | '1.3': 3 | commit: "0bf1790a7cc0174b84f8c25dade6b221b13d65e9" 4 | -------------------------------------------------------------------------------- /dev/releases/1.4.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Chungha" 2 | '1.4': 3 | commit: "981c0e4ae2ad1bff5a0778b6fa34916b0c4d4f4a" 4 | -------------------------------------------------------------------------------- /dev/releases/1.5.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Seulgi" 2 | '1.5': 3 | commit: "e282789153ec3df133474a56e8d922a73795b72a" 4 | -------------------------------------------------------------------------------- /dev/releases/2.0.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Irene" 2 | '2.0': 3 | commit: "55621ef4efdf61c3092d42565e897dfbaa0244c8" 4 | notes: 5 | - "[Architecture] Refactored into Python Package" 6 | -------------------------------------------------------------------------------- /dev/releases/2.1.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Jennie" 2 | 2.1.0: 3 | commit: "b87379ed986640788201f1ff52826413067e5ffb" 4 | 2.1.4: 5 | commit: "c95ce17451cb19b4775a819f82a532d3a3a6231b" 6 | -------------------------------------------------------------------------------- /dev/releases/2.10.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Yeri" 2 | 2.10.0: 3 | commit: "ce9d882856be8f6caca14ab7e5b9f13d6c31940b" 4 | 2.10.1: 5 | commit: "f555ee9d9fc485c6a241f4a8fa88bd68527ed2e2" 6 | 2.10.2: 7 | commit: "9a6617b4b1117a6e53d818aadcda886c831e16db" 8 | 2.10.3: 9 | commit: "5b7d1fd8e9f70a8bf9c5bfbe4aca5b796578e114" 10 | 2.10.4: 11 | commit: "3cf0dd9767fe62702b2f5c4f0267a234338a972b" 12 | 2.10.5: 13 | commit: "034bd064f11ef18ebbfcb25bd8acac8eacce1324" 14 | 2.10.6: 15 | commit: "f62fd254dd44deca50a860f3a966390ae9c3662c" 16 | 2.10.7: 17 | commit: "212fbf368e38281f45a0d8dd63dc051dbd8cd8cf" 18 | -------------------------------------------------------------------------------- /dev/releases/2.11.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Akali" 2 | 2.11.0: 3 | commit: "218313f80c160f90b28d99236a062ef62db7260d" 4 | -------------------------------------------------------------------------------- /dev/releases/2.12.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Tzuyu" 2 | 2.12.0: 3 | commit: "723efcb8ba12f7bda9acc21d81d9930265881c15" 4 | 2.12.1: 5 | commit: "a42ed56d2de47f88f873737f0f1374e99be895bf" 6 | 2.12.2: 7 | commit: "5006ad2bf1a6d5132ed07d28a7f6f0a9a454d5a7" 8 | 2.12.3: 9 | commit: "2a5d9498d1dd7bb6ac62a27d518a87542ec3f344" 10 | 2.12.4: 11 | commit: "06c32e205e95df8d3d1e27876887b7da7aa2bdf4" 12 | 2.12.5: 13 | commit: "c2f8ecc2dfa1ac4febde228fce150e08fb47be38" 14 | 2.12.6: 15 | commit: "a652a22a96ab693a4e7c3271520e6ad79fa025af" 16 | 2.12.7: 17 | commit: "5455abd0d1c9ecc8e000d04257f721babacb18e9" 18 | 2.12.8: 19 | commit: "8ebd27ab76ad2dcaf1dfef0cc171900fa20d5ee5" 20 | 2.12.9: 21 | commit: "49598e914f4e2d7895959bea954238db8a6cee78" 22 | 2.12.10: 23 | commit: "9037403777faa089092cf103181027febf6f0340" 24 | 2.12.12: 25 | commit: "eaaa0f3b53aa102ba1eb709c1803e94752017d86" 26 | 2.12.13: 27 | commit: "c31770a34c96cecc87af78f861819cc49fe98dda" 28 | 2.12.14: 29 | commit: "5157ce825eea9bf7b74123cb02dd28e25c6a0767" 30 | 2.12.15: 31 | commit: "33af60ed2c8c980f17338827a9cb96e7f2fd2572" 32 | 2.12.16: 33 | commit: "26dfdfb569d0beaf4ba8c6c67a9e2295d1362eed" 34 | 2.12.17: 35 | commit: "21012234409c01fec3cb5c506f0b4ba74b735b0b" 36 | 2.12.18: 37 | commit: "59eaa2264aefa6c9ed5f38e8490f77150bcae27b" 38 | 2.12.19: 39 | commit: "8958eb1b547f07d5d063c46cbe59ec57e000ecae" 40 | 2.12.20: 41 | commit: "7774d9a9361db986092e143e1bc397ce7a7524dd" 42 | -------------------------------------------------------------------------------- /dev/releases/2.13.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Aqua" 2 | 2.13.0: 3 | commit: "8555b28fbc9a220577260014b7f71f433263cb9f" 4 | 2.13.1: 5 | commit: "cefed03bc95dd5641b918f79b6ed14b2bfc9898d" 6 | 2.13.2: 7 | commit: "0f5ccd4645ead8d1ad48a532752d401424edb236" 8 | 2.13.3: 9 | commit: "40648b66f36894a297633c650e570ac77555d143" 10 | 2.13.4: 11 | commit: "0ccd39ffd99b19e4cd1b1a14f97bfb4385662eeb" 12 | -------------------------------------------------------------------------------- /dev/releases/2.14.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Mina" 2 | 2.14.0: 3 | commit: "1b0e3ffdb2389ae6ca484c78840756d0b7e5c0be" 4 | 2.14.1: 5 | commit: "fb2dff8addc7eaf740c5e30cbcd6791aab882c56" 6 | 2.14.2: 7 | commit: "cd8e0ff90abf7d01761b0576c4168254b9b1f7c1" 8 | 2.14.3: 9 | commit: "f806fb8ed24dd1474b80e7b1a9a7637cdbd35905" 10 | 2.14.4: 11 | commit: "868b8396a0a4ff0f687e651772d746af6d9dfab1" 12 | 2.14.5: 13 | commit: "21d1643988e40a02531bcc708f43925789d854d1" 14 | 2.14.6: 15 | commit: "ccbb3d3a807fd77a1481f9d44f311c7f8df659c7" 16 | 2.14.7: 17 | commit: "634df2ffafdfa00b6caf981108d333e30bf160f8" 18 | 2.14.8: 19 | commit: "ec5723d2b3122faaa5b76c5a1b156c9a915af9d6" 20 | 2.14.9: 21 | commit: "2c73c81434e1a591685a4b1d267a9eb6dbd57174" 22 | 2.14.10: 23 | commit: "e152a2edde836f8fb30427d13eb1e9e0d591a00b" 24 | -------------------------------------------------------------------------------- /dev/releases/2.2.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Rosé" 2 | 2.2.0: 3 | commit: "33cea26a791e224625aa9bc523e2cf90e39c8a50" 4 | 2.2.1: 5 | commit: "fbce600c4edd2b530e6673b89513b1a26b068b64" 6 | 2.2.2: 7 | commit: "c518627670f5614a2b9931471337a1a6b2ee344f" 8 | 2.2.3: 9 | commit: "a2cc27ddd46c7cf9959f33478eac396e18f90055" 10 | 2.2.4: 11 | commit: "c6deb1543779ce8b09af6bcbdc35e7668af86010" 12 | -------------------------------------------------------------------------------- /dev/releases/2.3.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Nancy" 2 | 2.3.0: 3 | commit: "8793b149f501fe5f3e237d7ae0fcd23c8f4e5e9d" 4 | 2.3.1: 5 | commit: "7c6e2ad60f15d8c4ac85a0808a0abd07549a4a2b" 6 | 2.3.2: 7 | commit: "5a08fd78c69c4047b82ff9c394ea23d25356758e" 8 | 2.3.3: 9 | commit: "9cf1fb3ed83817168dfe2ac30a42dcadb080c043" 10 | 2.3.4: 11 | commit: "eb82282e58259b243958e7590506bd26f8e92db0" 12 | 2.3.5: 13 | commit: "a4f13f6923b7783509462944f1abb235b4a068d0" 14 | 2.3.6: 15 | commit: "b611387011e4cbd274e210d0c21c83d15302281c" 16 | 2.3.7: 17 | commit: "b17060184b6897b18cf8af28a3817c9989aac96f" 18 | 2.3.8: 19 | commit: "afe01c8341acd4cf9f4b84fbba85aab6777fd230" 20 | -------------------------------------------------------------------------------- /dev/releases/2.4.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Songhee" 2 | 2.4.0: 3 | commit: "6aa65bf1ce273d9fd36d44f6e24439981b2228a3" 4 | 2.4.1: 5 | commit: "b117e6f7ec80afc6210314ce97bac087d5ab7b54" 6 | 2.4.2: 7 | commit: "d989134e65c20ab33b0ea8e4a132655074057757" 8 | 2.4.3: 9 | commit: "9b787fa3b13d77a9cfbe21061f519defac7fafd0" 10 | 2.4.4: 11 | commit: "948772b1c26070d7814871824b970fb60fc6976d" 12 | 2.4.5: 13 | commit: "2da5ab83b3a410b02af48e70b298069218a7e2a3" 14 | 2.4.6: 15 | commit: "65f9c88da4d56df37e4a3f974d7f660502c7a310" 16 | 2.4.7: 17 | commit: "c166620d5f9706e54f9cd67044d42bf8583575d8" 18 | 2.4.8: 19 | commit: "98c1527f778958b1a3322a4f026cfe2c421388aa" 20 | 2.4.9: 21 | commit: "b21b27bb6e230901281bb524f84e177c937b48fd" 22 | 2.4.10: 23 | commit: "08fe4695f6d5ef09789688481db478d0decbd5df" 24 | 2.4.11: 25 | commit: "5c6a901f5118be54ae44affbd6881b14bc30e04a" 26 | 2.4.12: 27 | commit: "6658165baedeee3939084ba4500de3de06bbc045" 28 | 2.4.13: 29 | commit: "57403a89ab1d679523341d6a607d0b03e495ff35" 30 | -------------------------------------------------------------------------------- /dev/releases/2.5.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Seungeun" 2 | 2.5.0: 3 | commit: "990131f546876d1461bac745e5cab3e60c78d038" 4 | 2.5.1: 5 | commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c" 6 | 2.5.2: 7 | commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c" 8 | 2.5.3: 9 | commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c" 10 | -------------------------------------------------------------------------------- /dev/releases/2.6.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "HyunA" 2 | 2.6.0: 3 | commit: "b161da1c1a1632725a44e998ff0d1872b3d5d184" 4 | 2.6.1: 5 | commit: "1eae55e3bba335d41da0d21dfc383b838d9f0d03" 6 | 2.6.2: 7 | commit: "dd3c83920b668466f2c053434bfd6be93bf32942" 8 | 2.6.3: 9 | commit: "27f3ff6d085f42bdb67385f967db904022339d1d" 10 | 2.6.4: 11 | commit: "5f8e73e6c714e9ca94a66f48d1b72fe516bbb0da" 12 | 2.6.5: 13 | commit: "0fdd7669cced6c2b47f657e510bda03a053ee7ae" 14 | 2.6.6: 15 | commit: "87cdb9987efe08b466f99f9ccb8b808131f9fbcd" 16 | 2.6.7: 17 | commit: "0bdc4654bfb0f42d838e15c3d36dab0b4472db00" 18 | 2.6.8: 19 | commit: "bdfb2a4a0b48362aabda7bb735296d83a02b932d" 20 | 2.6.9: 21 | commit: "cb7a6d224152048176e6187ede6d60625961ab39" 22 | -------------------------------------------------------------------------------- /dev/releases/2.7.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Shanshan" 2 | 2.7.0: 3 | commit: "8d7fb9a2c8be3f813ee5994be1818f9f81088faa" 4 | 2.7.1: 5 | commit: "6885fbdeccb8b690fa0af59d8fd341e44803798f" 6 | 2.7.2: 7 | commit: "4113d1761e28a7ee3b3cdabe4404cf3876f1fc84" 8 | 2.7.3: 9 | commit: "1563a15abde175022b50fa085c6b9b19a6021c31" 10 | 2.7.4: 11 | commit: "3e6bcc45d55446c6607664e407768391b47c5421" 12 | 2.7.5: 13 | commit: "fa05c406606e269fb4153465611caeb71c12b486" 14 | 2.7.6: 15 | commit: "47087b4288cbfa6000ca019a000f27ee5846d161" 16 | 2.7.7: 17 | commit: "379ee49f1c61df9720346d3d021dea040587d54d" 18 | 2.7.8: 19 | commit: "75bd823ad0cc24efecd1de193436a28dfaecd4f3" 20 | 2.7.9: 21 | commit: "fb04dd507cee42092b889fe72cdf9975ea48e3b1" 22 | 2.7.10: 23 | commit: "7fc879f77818371721e21c13e9df98796cf632de" 24 | 2.7.11: 25 | commit: "44a2739a3b6e58cb90b7f7dfca2197834cf30464" 26 | -------------------------------------------------------------------------------- /dev/releases/2.8.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Haeun" 2 | 2.8.0: 3 | commit: "25661f82af9338a024aae429cdafec7c86692aa5" 4 | 2.8.1: 5 | commit: "1321fcb45ebe0291c9fd47ff2eb8cc329035acf3" 6 | 2.8.2: 7 | commit: "e27a83bdc99a06a207c67c6f0034bc0a554c89af" 8 | 2.8.3: 9 | commit: "6acab324dbd3594dcfbf944bfdfb5c8fe173354b" 10 | 2.8.4: 11 | commit: "f7f1b1225e64b54d8962467182bddcc1de237f51" 12 | 2.8.5: 13 | commit: "1dbc0b7fca05830d654076c74a91b6b74f470d5b" 14 | -------------------------------------------------------------------------------- /dev/releases/2.9.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Yaorenmao" 2 | 2.9.0: 3 | commit: "8b4e9609e994d74506fd91471bd5a622b75b2f08" 4 | 2.9.1: 5 | commit: "52a9faae90175841b2c259dd4677697e513e12f9" 6 | 2.9.2: 7 | commit: "5cf7ca2e9bbf66082c4afb76b4033ff17c9cf8c8" 8 | 2.9.3: 9 | commit: "e8c316f1992c3e5f171891272f32d959bb1fa4f0" 10 | 2.9.4: 11 | commit: "e8a87cb8a5e2f63850ff3c02ed5aa8ee388460ed" 12 | 2.9.5: 13 | commit: "09d3f103832bb7e26949a8f2df60c25851886bdc" 14 | 2.9.6: 15 | commit: "9fb352cc6fe2bc41c56304e5ba941035fc1ac82d" 16 | 2.9.7: 17 | commit: "f4a563f080f7dba336034feb1c0c42057f8d8d8c" 18 | 2.9.8: 19 | commit: "2da9f154be240b8648d68a7eb2a3291738cfc93c" 20 | 2.9.9: 21 | commit: "f7861c44b4a44b0cdd34e9f3f62530b8bf2837e3" 22 | 2.9.10: 23 | commit: "22172d8b57df2ad1282f8d835183be45843fdd6a" 24 | -------------------------------------------------------------------------------- /dev/releases/3.0.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Yeonhee" 2 | 3.0.0: 3 | commit: "f31c95228eb2dc01e661be928ffd881c063377da" 4 | notes: 5 | - "[Architecture] Switched to SQLite for main database" 6 | - "[Architecture] Switched to SQLite for artwork cache" 7 | - "[Feature] Added scrobble deletion from web interface" 8 | 3.0.1: 9 | commit: "700b81217cb585df631d6f069243c56074cd1b71" 10 | notes: 11 | - "[Bugfix] Fixed upgrading imported scrobbles" 12 | 3.0.2: 13 | commit: "4a8221f7a08f679b21c1fb619f03e5f922a1dc2b" 14 | notes: 15 | - "[Logging] Cleaned up output for waitress warnings" 16 | - "[Bugfix] Fixed exception in native API" 17 | 3.0.3: 18 | commit: "1d9247fc724d7410b6e50d2cbfaa8f375d5e70af" 19 | notes: 20 | - "[Documentation] Added descriptions for native API endpoints" 21 | - "[Code Health] Made arguments for native API scrobbling explicit" 22 | - "[Bugfix] Fixed faulty entity type recognition for artists including the string 'artists'" 23 | - "[Bugfix] Fixed OS return codes" 24 | 3.0.4: 25 | commit: "206ebd58ea204e0008f2c9bf72d76dd9918fec53" 26 | notes: 27 | - "[Feature] Enabled dual stack for web server" 28 | - "[Feature] Added better feedback to native API endpoints" 29 | - "[Bugfix] Fixed native API receiving superfluous keywords" 30 | - "[Bugfix] Fixed crash when importing scrobbles with artists with similar names" 31 | 3.0.5: 32 | commit: "fe21894c5ecf3a53c9c5c00453abfc7f41c6a83e" 33 | notes: 34 | - "[Feature] Added notification system for web interface" 35 | - "[Bugfix] Fixed crash when encountering error in Lastfm import" 36 | 3.0.6: 37 | commit: "b3d4cb7a153845d1f5a5eef67a6508754e338f2f" 38 | notes: 39 | - "[Performance] Implemented search in database" 40 | - "[Bugfix] Better parsing of featuring artists" 41 | - "[Bugfix] Fixed buffered output in Docker" 42 | - "[Bugfix] Fixed importing a Spotify file without path" 43 | - "[Bugfix] No longer releasing database lock during scrobble creation" 44 | - "[Distribution] Experimental arm64 image" 45 | 3.0.7: 46 | commit: "62abc319303a6cb6463f7c27b6ef09b76fc67f86" 47 | notes: 48 | - "[Bugix] Improved signal handling" 49 | - "[Bugix] Fixed constant re-caching of all-time stats, significantly increasing page load speed" 50 | - "[Logging] Disabled cache information when cache is not used" 51 | - "[Distribution] Experimental arm/v7 image" 52 | -------------------------------------------------------------------------------- /dev/releases/3.1.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Soyeon" 2 | 3.1.0: 3 | commit: "bfa553bed05d7dba33f611a44485d6cf460ba308" 4 | notes: 5 | - "[Architecture] Cleaned up legacy process control" 6 | - "[Architecture] Added proper exception framework to native API" 7 | - "[Feature] Implemented track title and artist name editing from web interface" 8 | - "[Feature] Implemented track and artist merging from web interface" 9 | - "[Feature] Implemented scrobble reparsing from web interface" 10 | - "[Performance] Adjusted cache sizes" 11 | - "[Logging] Added cache memory use information" 12 | - "[Technical] Bumped Python Version and various dependencies" 13 | 3.1.1: 14 | commit: "20aae955b2263be07c56bafe4794f622117116ef" 15 | notes: 16 | - "[Bugfix] Fixed inclusion of custom css files" 17 | - "[Bugfix] Fixed list values in configuration" 18 | 3.1.2: 19 | commit: "a0739306013cd9661f028fb5b2620cfa2d298aa4" 20 | notes: 21 | - "[Feature] Added remix artist parsing" 22 | - "[Feature] Added API debug mode" 23 | - "[Bugfix] Fixed leftover whitespaces when parsing titles" 24 | - "[Bugfix] Fixed handling of fallthrough values in config file" 25 | 3.1.3: 26 | commit: "f3a04c79b1c37597cdf3cafcd95e3c923cd6a53f" 27 | notes: 28 | - "[Bugfix] Fixed infinite recursion with capitalized featuring delimiters" 29 | - "[Bugfix] Fixed favicon display" 30 | 3.1.4: 31 | commit: "ef06f2262205c903e7c3060e2d2d52397f8ffc9d" 32 | notes: 33 | - "[Feature] Expanded information saved from Listenbrainz API" 34 | - "[Feature] Added import for Listenbrainz exports" 35 | - "[Bugfix] Sanitized artists and tracks with html-like structure" 36 | 3.1.5: 37 | commit: "4330b0294bc0a01cdb841e2e3db370108da901db" 38 | notes: 39 | - "[Feature] Made image upload part of regular API" 40 | - "[Bugfix] Additional entity name sanitization" 41 | - "[Bugfix] Fixed image display on Safari" 42 | - "[Bugfix] Fixed entity editing on Firefox" 43 | - "[Bugfix] Made compatibile with SQLAlchemy 2.0" 44 | -------------------------------------------------------------------------------- /dev/releases/3.2.yml: -------------------------------------------------------------------------------- 1 | minor_release_name: "Nicole" 2 | 3.2.0: 3 | commit: "34d0a49eb8deae2fb95233289521bb817732c772" 4 | notes: 5 | - "[Architecture] Switched to linuxserver.io container base image" 6 | - "[Architecture] Reworked image handling" 7 | - "[Architecture] Removed pre-calculated stats" 8 | - "[Feature] Added support for albums" 9 | - "[Feature] New start page" 10 | - "[Feature] Added UI for track-artist, track-album and album-artist association" 11 | - "[Feature] Added inline UI for association and merging in chart lists" 12 | - "[Feature] Added UI selector for including associated artists" 13 | - "[Feature] Added UI distinction for associated scrobbles in chart bars" 14 | - "[Performance] Improved image rendering" 15 | - "[Performance] Optimized several database calls" 16 | - "[Bugfix] Fixed configuration of time format" 17 | - "[Bugfix] Fixed search on manual scrobble page" 18 | - "[Bugfix] Disabled DB maintenance while not running main server" 19 | - "[Bugfix] Removed some nonsensical ephemereal database entry creations" 20 | - "[Bugfix] Fixed API endpoint for track charts with no artist provided" 21 | - "[Technical] Bumped Python and SQLAlchemy versions" 22 | - "[Distribution] Removed build of arm/v7 image" 23 | 3.2.1: 24 | commit: "5495d6e38d95c0c2128e1de9a9553b55b6be945b" 25 | notes: 26 | - "[Feature] Added setting for custom week offset" 27 | - "[Feature] Added Musicbrainz album art fetching" 28 | - "[Bugfix] Fixed album entity rows being marked as track entity rows" 29 | - "[Bugfix] Fixed scrobbling of tracks when all artists have been removed by server parsing" 30 | - "[Bugfix] Fixed Spotify import of multiple files" 31 | - "[Bugfix] Fixed process control on FreeBSD" 32 | - "[Bugfix] Fixed Spotify authentication thread blocking the process from terminating" 33 | - "[Technical] Upgraded all third party modules to use requests module and send User Agent" 34 | 3.2.2: 35 | commit: "febaff97228b37a192f2630aa331cac5e5c3e98e" 36 | notes: 37 | - "[Security] Fixed XSS vulnerability in error page (Disclosed by https://github.com/NULLYUKI)" 38 | - "[Architecture] Reworked the default directory selection" 39 | - "[Feature] Added option to show scrobbles on tile charts" 40 | - "[Bugfix] Fixed Last.fm authentication" 41 | 3.2.3: 42 | commit: "a7dcd3df8a6b051a1f6d0b7d10cc5af83502445c" 43 | notes: 44 | - "[Architecture] Upgraded doreah, significant rework of authentication" 45 | - "[Bugfix] Fixed initial permission check" 46 | - "[Bugfix] Fixed and updated various texts" 47 | - "[Bugfix] Fixed moving tracks to different album" 48 | 3.2.4: 49 | notes: 50 | - "[Architecture] Removed daemonization capabilities" 51 | - "[Architecture] Moved import to main server process" 52 | - "[Feature] Implemented support for ghan's csv Last.fm export" 53 | - "[Performance] Debounced search" 54 | - "[Bugfix] Fixed stuck scrobbling from Navidrome" 55 | - "[Bugfix] Fixed missing image mimetype" 56 | - "[Technical] Pinned dependencies" 57 | - "[Technical] Upgraded Python and Alpine" -------------------------------------------------------------------------------- /dev/templates/requirements.txt.jinja: -------------------------------------------------------------------------------- 1 | {% for dep in project.dependencies -%} 2 | {{ dep }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /dev/templates/requirements_extra.txt.jinja: -------------------------------------------------------------------------------- 1 | {% for dep in project['optional-dependencies'].full -%} 2 | {{ dep }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /dev/testing/stresstest.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import subprocess 3 | import time 4 | import requests 5 | import os 6 | 7 | ACTIVE = True 8 | 9 | build_cmd = ["docker","build","-t","maloja",".","-f","Containerfile"] 10 | subprocess.run(build_cmd) 11 | 12 | common_prc = ( 13 | ["docker","run","--rm","-v",f"{os.path.abspath('./testdata')}:/mlj","-e","MALOJA_DATA_DIRECTORY=/mlj"], 14 | ["maloja"] 15 | ) 16 | 17 | servers = [ 18 | {'port': 42010}, 19 | {'port': 42011, 'extraargs':["--memory=1g"]}, 20 | {'port': 42012, 'extraargs':["--memory=500m"]} 21 | ] 22 | for s in servers: 23 | cmd = common_prc[0] + ["-p",f"{s['port']}:42010"] + s.get('extraargs',[]) + common_prc[1] 24 | print(cmd) 25 | t = threading.Thread(target=subprocess.run,args=(cmd,)) 26 | s['thread'] = t 27 | t.daemon = True 28 | t.start() 29 | time.sleep(5) 30 | 31 | time.sleep(5) 32 | while ACTIVE: 33 | time.sleep(1) 34 | try: 35 | for s in servers: 36 | requests.get(f"http://localhost:{s['port']}") 37 | except KeyboardInterrupt: 38 | ACTIVE = False 39 | except Exception: 40 | pass 41 | 42 | for s in servers: 43 | s['thread'].join() 44 | -------------------------------------------------------------------------------- /dev/update_dist_files.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create necessary files from sources of truth. Currently just the requirements.txt files. 3 | """ 4 | 5 | import toml 6 | import os 7 | import jinja2 8 | 9 | env = jinja2.Environment( 10 | loader=jinja2.FileSystemLoader('./templates'), 11 | autoescape=jinja2.select_autoescape(['html', 'xml']), 12 | keep_trailing_newline=True 13 | ) 14 | 15 | with open("../pyproject.toml") as filed: 16 | data = toml.load(filed) 17 | 18 | templatedir = "./templates" 19 | 20 | for root,dirs,files in os.walk(templatedir): 21 | 22 | reldirpath = os.path.relpath(root,start=templatedir) 23 | for f in files: 24 | 25 | relfilepath = os.path.join(reldirpath,f) 26 | 27 | if not f.endswith('.jinja'): continue 28 | 29 | srcfile = os.path.join(root,f) 30 | trgfile = os.path.join("..", reldirpath,f.replace(".jinja","")) 31 | 32 | 33 | template = env.get_template(relfilepath) 34 | result = template.render(**data) 35 | 36 | with open(trgfile,"w") as filed: 37 | filed.write(result) 38 | -------------------------------------------------------------------------------- /dev/update_scrobbler.sh: -------------------------------------------------------------------------------- 1 | ICON_DIR=./maloja/web/static/png; 2 | SCROBBLER_DIR=./auxiliary/chromium_scrobbler; 3 | 4 | convert $ICON_DIR/favicon_large.png -resize 256 $SCROBBLER_DIR/maloja-scrobbler/icon256.png 5 | convert $ICON_DIR/favicon_large.png -resize 128 $SCROBBLER_DIR/maloja-scrobbler/icon128.png 6 | convert $ICON_DIR/favicon_large.png -resize 48 $SCROBBLER_DIR/maloja-scrobbler/icon48.png 7 | convert $ICON_DIR/favicon_large.png -background none -resize 280 -gravity center -extent 440x280 -background "#232327" -flatten $SCROBBLER_DIR/tile.png 8 | rm $SCROBBLER_DIR/maloja-scrobbler.zip 9 | zip $SCROBBLER_DIR/maloja-scrobbler.zip $SCROBBLER_DIR/maloja-scrobbler/* $SCROBBLER_DIR/maloja-scrobbler/*/* 10 | -------------------------------------------------------------------------------- /dev/write_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read the changelogs / version metadata and create all git tags 3 | """ 4 | 5 | import os 6 | import subprocess as sp 7 | import yaml 8 | 9 | FOLDER = "dev/releases" 10 | 11 | releases = {} 12 | for f in os.listdir(FOLDER): 13 | if f == "branch.yml": continue 14 | #maj,min = (int(i) for i in f.split('.')[:2]) 15 | 16 | with open(os.path.join(FOLDER,f)) as fd: 17 | data = yaml.safe_load(fd) 18 | 19 | name = data.pop('minor_release_name') 20 | 21 | for tag in data: 22 | tagtup = tuple(int(i) for i in tag.split('.')) 23 | releases[tagtup] = data[tag] 24 | 25 | # this is a bit dirty, works on our data 26 | if len(tagtup)<3 or tagtup[2] == 0: releases[tagtup]['name'] = name 27 | 28 | 29 | for version in releases: 30 | 31 | info = releases[version] 32 | version = '.'.join(str(v) for v in version) 33 | msg = [ 34 | f"Version {version}" + (f" '{info.get('name')}'" if info.get('name') else ''), 35 | *([""] if info.get('notes') else []), 36 | *[f"* {n}" for n in info.get('notes',[])] 37 | ] 38 | 39 | 40 | cmd = [ 41 | 'git','tag','--force', 42 | '-a',f'v{version}', 43 | '-m', 44 | '\n'.join(msg), 45 | info['commit'] 46 | ] 47 | 48 | try: 49 | prev_tag = sp.check_output(["git","show",f'v{maj}.{min}.{hot}']).decode() 50 | prev_tag_commit = prev_tag.split('\n')[6].split(" ")[1] 51 | except Exception: 52 | pass 53 | else: 54 | assert prev_tag_commit == info['commit'] 55 | 56 | print(cmd) 57 | sp.run(cmd) 58 | -------------------------------------------------------------------------------- /example-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | maloja: 3 | # from dockerhub 4 | image: "krateng/maloja:latest" 5 | # or built locally 6 | #build: 7 | # context: . 8 | # dockerfile: ./Containerfile 9 | ports: 10 | - "42010:42010" 11 | # different directories for configuration, state and logs 12 | volumes: 13 | - "$PWD/config:/etc/maloja" 14 | - "$PWD/data:/var/lib/maloja" 15 | - "$PWD/logs:/var/log/maloja" 16 | #you can also have everything together instead: 17 | #volumes: 18 | #- "$PWD/data:/data" 19 | #environment: 20 | #- "MALOJA_DATA_DIRECTORY=/data" 21 | -------------------------------------------------------------------------------- /maloja/__init__.py: -------------------------------------------------------------------------------- 1 | # monkey patching 2 | from .pkg_global import monkey 3 | # configuration before all else 4 | from .pkg_global import conf 5 | -------------------------------------------------------------------------------- /maloja/__pkginfo__.py: -------------------------------------------------------------------------------- 1 | # This file has now been slighly repurposed and will simply give other parts of 2 | # the package access to some global meta-information about itself 3 | 4 | # you know what f*ck it 5 | # this is hardcoded for now because of that damn project / package name discrepancy 6 | # i'll fix it one day 7 | VERSION = "3.2.4" 8 | HOMEPAGE = "https://github.com/krateng/maloja" 9 | 10 | 11 | USER_AGENT = f"Maloja/{VERSION} ( {HOMEPAGE} )" 12 | -------------------------------------------------------------------------------- /maloja/apis/__init__.py: -------------------------------------------------------------------------------- 1 | from ._apikeys import apikeystore 2 | 3 | 4 | import copy 5 | from bottle import redirect, request, response 6 | from urllib.parse import urlencode 7 | 8 | 9 | 10 | def init_apis(server): 11 | 12 | from . import native_v1 13 | from .audioscrobbler import Audioscrobbler 14 | from .audioscrobbler_legacy import AudioscrobblerLegacy 15 | from .listenbrainz import Listenbrainz 16 | 17 | native_apis = [ 18 | native_v1.api 19 | ] 20 | standardized_apis = [ 21 | Listenbrainz(), 22 | Audioscrobbler(), 23 | AudioscrobblerLegacy() 24 | ] 25 | 26 | for api in native_apis: 27 | api.mount(server=server,path="apis/"+api.__apipath__) 28 | 29 | for api in standardized_apis: 30 | aliases = api.__aliases__ 31 | canonical = aliases[0] 32 | api.nimrodelapi.mount(server=server,path="apis/" + canonical) 33 | 34 | # redirects 35 | for alias in aliases[1:]: 36 | altpath = "/apis/" + alias + "/" 37 | altpath_empty = "/apis/" + alias 38 | altpath_empty_cl = "/apis/" + alias + "/" 39 | 40 | def alias_api(pth=""): 41 | redirect("/apis/" + canonical + "/" + pth + "?" + urlencode(request.query)) 42 | 43 | server.get(altpath)(alias_api) 44 | server.post(altpath)(alias_api) 45 | server.get(altpath_empty)(alias_api) 46 | server.post(altpath_empty)(alias_api) 47 | server.get(altpath_empty_cl)(alias_api) 48 | server.post(altpath_empty_cl)(alias_api) 49 | 50 | def invalid_api(pth=''): 51 | response.status = 404 52 | return {"error":"Invalid API"} 53 | 54 | server.get("/apis/")(invalid_api) 55 | server.post("/apis/")(invalid_api) 56 | 57 | server.get("/apis")(invalid_api) 58 | server.post("/apis")(invalid_api) 59 | -------------------------------------------------------------------------------- /maloja/apis/_apikeys.py: -------------------------------------------------------------------------------- 1 | ### API KEYS 2 | ### symmetric keys are fine since we hopefully use HTTPS 3 | 4 | from doreah.keystore import KeyStore 5 | from doreah.logging import log 6 | 7 | from ..pkg_global.conf import data_dir 8 | 9 | apikeystore = KeyStore(file=data_dir['clients']("apikeys.yml"),save_endpoint="/apis/mlj_1/apikeys") 10 | 11 | 12 | from .. import upgrade 13 | upgrade.upgrade_apikeys() 14 | 15 | 16 | # skip regular authentication if api key is present in request 17 | # an api key now ONLY permits scrobbling tracks, no other admin tasks 18 | def api_key_correct(request,args,kwargs): 19 | if "key" in kwargs: 20 | apikey = kwargs.pop("key") 21 | elif "apikey" in kwargs: 22 | apikey = kwargs.pop("apikey") 23 | else: return False 24 | 25 | client = apikeystore.check_and_identify_key(apikey) 26 | if client: 27 | return {'client':client} 28 | else: 29 | return False 30 | -------------------------------------------------------------------------------- /maloja/apis/_exceptions.py: -------------------------------------------------------------------------------- 1 | class BadAuthException(Exception): pass 2 | class InvalidAuthException(Exception): pass 3 | class InvalidMethodException(Exception): pass 4 | class InvalidSessionKey(Exception): pass 5 | class MalformedJSONException(Exception): pass 6 | 7 | -------------------------------------------------------------------------------- /maloja/data_files/cache/.maloja_cache_sentinel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/cache/.maloja_cache_sentinel -------------------------------------------------------------------------------- /maloja/data_files/cache/images/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/cache/images/dummy -------------------------------------------------------------------------------- /maloja/data_files/config/.maloja_config_sentinel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/config/.maloja_config_sentinel -------------------------------------------------------------------------------- /maloja/data_files/config/custom_css/customcss.info: -------------------------------------------------------------------------------- 1 | In this folder, you can place any number of CSS files to change Maloja's style. 2 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/.gitignore: -------------------------------------------------------------------------------- 1 | !*.tsv 2 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_artistsingroups.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Artists and Groups 2 | # DESC: Combines artists that also performed as part of a group 3 | 4 | # single artist more famous than group 5 | countas Selena Gomez & The Scene Selena Gomez 6 | countas The Police Sting 7 | countas Trouble Maker HyunA 8 | countas S Club 7 Tina Barrett 9 | countas 4Minute HyunA 10 | countas I.O.I Chungha 11 | countas TrySail Sora Amamiya 12 | # Group more famous than single artist 13 | countas RenoakRhythm Approaching Nirvana 14 | countas Shirley Manson Garbage 15 | countas Lewis Brindley The Yogscast 16 | countas Sips The Yogscast 17 | countas Sjin The Yogscast 18 | countas Airi Suzuki ℃-ute 19 | countas CeeLo Green Gnarls Barkley 20 | countas Amelia Watson Hololive EN 21 | countas Gawr Gura Hololive EN 22 | countas Mori Calliope Hololive EN 23 | countas Ninomae Ina'nis Hololive EN 24 | countas Takanashi Kiara Hololive EN 25 | countas Ceres Fauna Hololive EN 26 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_classical.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Classical Music 2 | # DESC: Various standardizations for Classical Music 3 | 4 | replacetitle Tochter Zion freue dich Tochter Zion, freue dich 5 | replacetitle Zadok the Priest (Coronation Anthem No.1 HWV 258) Zadok the Priest 6 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_cpop.tsv: -------------------------------------------------------------------------------- 1 | # NAME: CPop 2 | # DESC: Romanizes some CPop artists and tracks 3 | 4 | replaceartist SING女團 S.I.N.G. 5 | replacetitle SING-Moonlight Thoughts Moonlight Thoughts (English Version) 6 | replacetitle 那不勒斯的黎明 Dawn in Naples 7 | countas Ju Jingyi SNH48 8 | countas 7SENSES SNH48 9 | countas BEJ48 SNH48 10 | countas Li Yitong SNH48 11 | countas Dai Meng SNH48 12 | countas Mo Han SNH48 13 | countas Su Shanshan SNH48 14 | countas Lin Hui S.I.N.G. 15 | replaceartist 鞠婧禕 Ju Jungyi 16 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_dach.tsv: -------------------------------------------------------------------------------- 1 | # NAME: DACH Artists 2 | # DESC: Some fixes for artists in Switzerland, Imperial Switzerland and Big Switzerland 3 | 4 | replaceartist Erste Allgemeine Verunsicherung EAV 5 | replaceartist E.A.V. EAV 6 | replaceartist E.A.V. (Erste Allgemeine Verunsicherung) EAV 7 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_firefly-soundtrack.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Firefly / Serenity Soundtracks 2 | # DESC: Fixes tracks and artists from the soundtracks of Firefly and Serenity 3 | 4 | # Firefly 5 | replacetitle Firefly Main Title The Ballad of Serenity 6 | replacetitle Firefly Main Theme The Ballad of Serenity 7 | replacetitle Firefly - Main Title The Ballad of Serenity 8 | replacetitle Firefly - Main Theme The Ballad of Serenity 9 | 10 | # Serenity 11 | replacetitle Jane & Zoe / Final Battle Jayne & Zoe / Final Battle 12 | replacetitle Jane & Zoe Jayne & Zoe 13 | replacetitle Sheperd Books' Last Words Shepherd Book's Last Words 14 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_jeremysoule.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Jeremy Soule 2 | # DESC: Fixes various different spellings and mistakes for Jeremy Soule's soundtracks 3 | 4 | # MORROWIND 5 | replacetitle Nerevar Rising (Morrowind Title Song) Nerevar Rising 6 | replacetitle Nerevar Rising (Morrowind Title Song Reprise) Nerevar Rising (Reprise) 7 | 8 | # OBLIVION 9 | replacetitle Elder Scrolls: Oblivion Reign of the Septims 10 | 11 | 12 | # SKYRIM 13 | 14 | 15 | # GUILD WARS 16 | replacetitle Guild Wars Nightfall Theme Land of the Golden Sun 17 | replacetitle Under the Dark Span (Asura Theme) Under the Dark Span 18 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_jpop.tsv: -------------------------------------------------------------------------------- 1 | # NAME: JPop 2 | # DESC: Fixes and romanizes various Japanese tracks and artists 3 | 4 | 5 | belongtogether Myth & Roid 6 | 7 | 8 | # Sora-chan 9 | replaceartist Amamiya Sora Sora Amamiya 10 | replacetitle エデンの旅人 Eden no Tabibito 11 | replacetitle 月灯り Tsukiakari 12 | replacetitle 火花 Hibana 13 | replacetitle ロンリーナイト・ディスコティック Lonely Night Discotheque 14 | replacetitle 羽根輪舞 Hane Rinbu 15 | replacetitle メリーゴーランド Merry-go-round 16 | replacetitle フリイジア Fressia 17 | replacetitle 誓い Chikai 18 | 19 | # ReoNa 20 | replacetitle ないない nainai 21 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_kpop.tsv: -------------------------------------------------------------------------------- 1 | # NAME: K-Pop 2 | # DESC: Some romanizations and fixes 3 | 4 | replaceartist 강남스타일 Gangnam Style 5 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_lotr-soundtrack.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Lord of the Rings Soundtrack 2 | # DESC: Fixes tracks and artists from Howard Shore's soundtrack on different releases 3 | 4 | replaceartist Howard Shore/Mabel Faletolu Howard Shore␟Mabel Faletolu 5 | replaceartist Sir James Galway Viggo Mortensen And Renee Fleming Sir James Galway␟Viggo Mortensen␟Renée Fleming 6 | replaceartist Sir James Galway Viggo Mortensen And Renée Fleming Sir James Galway␟Viggo Mortensen␟Renée Fleming 7 | replaceartist Billy Boyd And Dominic Monaghan Billy Boyd␟Dominic Monaghan 8 | replaceartist Billy Boyd and Dominic Monaghan Billy Boyd␟Dominic Monaghan 9 | replaceartist Billy Boyd And Dominic Monaghan Album Version Billy Boyd␟Dominic Monaghan 10 | replaceartist Sissel Performing Asëa Aranion Sissel 11 | replaceartist Annie Lennox Performing Into The West Annie Lennox 12 | replaceartist Billy Boyd Performing The Edge Of Night Billy Boyd 13 | replaceartist Liv Tyler Album Version Liv Tyler 14 | replaceartist Renée Fleming Album Version Renée Fleming 15 | replaceartist Ben del Maestro Album Version Ben Del Maestro 16 | replaceartist Sir James Galway Album Version Sir James Galway 17 | replaceartist Sir James Galway Viggo Mortensen And Renée Fleming Album Version Sir James Galway␟Viggo Mortensen␟Renée Fleming 18 | notanartist In Dreams 19 | notanartist Aniron Theme For Aragorn And Arwen 20 | notanartist Lament for Gandalf 21 | replaceartist James Galway Sir James Galway 22 | replaceartist Ben del Maestro Ben Del Maestro 23 | replacetitle Anduril Andúril 24 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_masseffect.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Mass Effect Soundtrack 2 | # DESC: Sets correct artists for the Mass Effect soundtracks 3 | 4 | # 1 5 | fixartists Jack Wall␟Sam Hulick Mass Effect Theme 6 | fixartists Richard Jacques␟Jack Wall␟Sam Hulick Spectre Induction 7 | fixartists Richard Jacques␟Jack Wall␟Sam Hulick The Citadel 8 | fixartists Richard Jacques␟Jack Wall The Thorian 9 | fixartists Richard Jacques␟Jack Wall␟Sam Hulick The Alien Queen 10 | fixartists Jack Wall␟Sam Hulick Breeding Ground 11 | fixartists Jack Wall␟Sam Hulick In Pursuit of Saren 12 | fixartists David Kates␟Jack Wall␟Sam Hulick Infusion 13 | fixartists David Kates␟Jack Wall␟Sam Hulick Final Assault 14 | 15 | # 2 16 | fixartists Jack Wall␟David Kates Thane 17 | fixartists Jack Wall␟Sam Hulick The Normandy Attacked 18 | fixartists Jack Wall␟Brian DiDomenico The Collector Base 19 | fixartists Jack Wall␟Sam Hulick New Worlds 20 | 21 | # 3 22 | fixartists Sascha Dikiciyan␟Cris Velasco The Ardat-Yakshi 23 | fixartists Sascha Dikiciyan␟Cris Velasco Rannoch 24 | fixartists Sascha Dikiciyan␟Cris Velasco I'm Sorry 25 | fixartists Sascha Dikiciyan␟Cris Velasco The Scientists 26 | fixartists Sascha Dikiciyan␟Cris Velasco Aralakh Company 27 | fixartists Sascha Dikiciyan␟Cris Velasco Prothean Beacon 28 | fixartists Sascha Dikiciyan␟Cris Velasco Reaper Chase 29 | fixartists Clint Mansell␟Sam Hulick An End, Once and For All 30 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_memes.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Meme Music 2 | # DESC: Some fixes for internet artists 3 | 4 | replaceartist Weebl's Stuff Mr Weebl 5 | replaceartist Mr. Weebl Mr Weebl 6 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_monstercat.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Monstercat EDM 2 | # DESC: Fixes parsing of some Monstercat releases 3 | 4 | belongtogether Darth & Vader 5 | belongtogether Case & Point 6 | replaceartist Mr FijiWiji, AgNO3 Mr FijiWiji␟AgNO3 7 | replaceartist Danilo Garcia, Laura Brehm Danilo Garcia␟Laura Brehm 8 | replaceartist Two Thirds Laura Brehm Two Thirds␟Laura Brehm 9 | replaceartist Approaching Nirvana Laura Brehm Approaching Nirvana␟Laura Brehm 10 | replaceartist Mr FijiWiji AgNO3 Mr FijiWiji␟AgNO3 11 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_redcliff.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Red Cliff 2 | # DESC: Simplifies titles of the Red Cliff OST 3 | 4 | replacetitle Red Cliff - River of No Return - (End Roll Version) / Theme Song Red Cliff - River of No Return 5 | replacetitle Red Cliff (End Roll Version) / Theme Song Part 1 (Asia Version) Red Cliff - End Roll Version 6 | fixartists Alan␟Taro Iwashiro Red Cliff - River of No Return 7 | fixartists Alan␟Taro Iwashiro Red Cliff - End Roll Version 8 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_specialsymbols.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Special Symbols 2 | # DESC: Defines artists with separators in their names 3 | 4 | belongtogether Megan & Liz 5 | belongtogether Bell, Book & Candle 6 | belongtogether K/DA 7 | belongtogether Darth & Vader 8 | belongtogether Case & Point 9 | belongtogether Selena Gomez & The Scene 10 | belongtogether Gerry & The Pacemakers 11 | belongtogether AC/DC 12 | belongtogether Au/Ra 13 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/krateng_threelions.tsv: -------------------------------------------------------------------------------- 1 | # NAME: Three Lions 2 | # DESC: Fixes the countless variations of Three Lions titles 3 | 4 | replaceartist Lightning Seeds The Lightning Seeds 5 | replaceartist Baddiel, Skinner Baddiel␟Skinner 6 | replacetitle Three Lions '98 (Single Version) (With Commentary) Three Lions '98 7 | replacetitle Three Lions - Original Version Three Lions '96 8 | replacetitle Three Lions '98 - Single Version (with commentary) Three Lions '98 9 | fixartists Baddiel␟Skinner␟The Lightning Seeds Three Lions '98 10 | fixartists Baddiel␟Skinner␟The Lightning Seeds Three Lions '96 11 | replaceartist David Baddiel Baddiel 12 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/predefined.info: -------------------------------------------------------------------------------- 1 | These are some files to get you started if you don't want to create your own rules. 2 | They are concerned with specific artists / genres and are not complete by any means, 3 | but may help you have a clean database from the start if your music taste happens 4 | to be met by them. If you would like to curate your own ruleset, create a pull request 5 | on GitHub. Please do not simply contribute your complete rule file, but a selection 6 | concerned with a specific genre / category / group of music. 7 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/predefined/tdemin_kpop.tsv: -------------------------------------------------------------------------------- 1 | # NAME: LOONA 2 | # DESC: Groups various side projects together 3 | 4 | # LOONA 5 | belongtogether LOOΠΔ / ODD EYE CIRCLE 6 | belongtogether LOOΠΔ 1/3 7 | belongtogether LOONA/yyxy 8 | countas LOOΠΔ / ODD EYE CIRCLE LOOΠΔ 9 | countas LOOΠΔ 1/3 LOOΠΔ 10 | countas LOONA/yyxy LOOΠΔ 11 | countas Chuu LOOΠΔ 12 | countas JinSoul LOOΠΔ 13 | replaceartist LOONA LOOΠΔ 14 | -------------------------------------------------------------------------------- /maloja/data_files/config/rules/rules.info: -------------------------------------------------------------------------------- 1 | This folder can have any number of tsv files to group your rules 2 | 3 | The first column defines the type of the rule: 4 | notanartist Defines strings that can appear behind "feat" in a song title, but denote additional information about the track instead of another artist. 5 | Second column is the string 6 | belongtogether Defines an artist with an ampersand or other delimiter in their name. Otherwise, the artist string will be interpreted as two different artists (except when there are no spaces). 7 | This artist will be accepted without further parsing. If you want to replace the spelling, you need a replaceartist entry instead 8 | Second column is the full name of the artist 9 | replacetitle Defines an alternative spelling of a track title that should be replaced. 10 | Second column is the 'wrong' spelling. Capitalization is ignored. 11 | Third column the correct spelling 12 | replaceartist Defines and alternative spelling of an artist that should be replaced 13 | Second column is the 'wrong' spelling. Capitalization is ignored. 14 | Third column the correct spelling. Use ␟ if the spelling should correct to several artists 15 | countas Defines an artist that should be counted together with another artist for chart statistics etc. 16 | This will not change the separation in the database and all effects of this rule will disappear as soon as it is no longer active. 17 | Second column is the artist 18 | Third column the replacement artist / grouping label 19 | addartists Defines a certain combination of artists and song title that should always have other artists added. 20 | Second column is artists that need to be already present for this rule to apply 21 | Third column is the song title 22 | Fourth column are artists that shoud be added, separated by ␟ 23 | fixartists Similar as above, but simply specifies that if any of the given artists is present, all (and no others) should be present 24 | Second column is correct artists 25 | Third column is the song title 26 | artistintitle Defines title strings that imply the presence of another artist. 27 | Second column is the string 28 | Third column is the artist or artists 29 | 30 | Rules in non-tsv files are ignored. '#' is used for comments. Additional columns are ignored. To have a '#' in a name, use '\num' 31 | Comments are not supported in scrobble lists, but you probably never edit these manually anyway. 32 | 33 | An example file could look like this: 34 | 35 | 36 | notanartist In Dreams 37 | belongtogether Darth & Vader 38 | belongtogether AC/DC # / and ; separate even when there are no spaces, so make sure to add artists with those symbols 39 | replacetitle 첫 사랑니 (Rum Pum Pum Pum) Rum Pum Pum Pum 40 | replaceartist Dal Shabet Dal★Shabet 41 | replaceartist Mr FijiWiji, AgNO3 Mr FijiWiji␟AgNO3 # one artist is replaced by two artists 42 | countas Trouble Maker HyunA 43 | addartists HyunA Change Jun Hyung 44 | artistintitle Areia Remix Areia 45 | -------------------------------------------------------------------------------- /maloja/data_files/logs/.maloja_logs_sentinel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/logs/.maloja_logs_sentinel -------------------------------------------------------------------------------- /maloja/data_files/logs/dbfix/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/logs/dbfix/dummy -------------------------------------------------------------------------------- /maloja/data_files/state/.maloja_state_sentinel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/state/.maloja_state_sentinel -------------------------------------------------------------------------------- /maloja/data_files/state/auth/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/state/auth/dummy -------------------------------------------------------------------------------- /maloja/data_files/state/backups/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/state/backups/dummy -------------------------------------------------------------------------------- /maloja/data_files/state/images/albums/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/state/images/albums/dummy -------------------------------------------------------------------------------- /maloja/data_files/state/images/artists/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/state/images/artists/dummy -------------------------------------------------------------------------------- /maloja/data_files/state/images/images.info: -------------------------------------------------------------------------------- 1 | In this folder, you can save custom images for artists and tracks in case you 2 | don't like the ones provided by Last.FM, you don't want to use Last.FM or it's 3 | missing some artists / tracks that are important to you. 4 | 5 | For artists, you can use a .jpg, .jpeg, .png or .gif image that is named like 6 | the artist, but with all non-alphanumeric characters removed. You may also chose 7 | the safe route and omit everything but numbers and latin characters (except the 8 | dot separating the name and the extension). You can either capitalize exactly as 9 | the title or use only lower case, but not use arbitrary capizalization. In case 10 | you want to have multiple images of the same format, you may create a folder 11 | named like the artist and add any images inside. So, for example, if your artist 12 | is "Dal★Shabet", all these files would be considered: 13 | 14 | DalShabet.jpg 15 | dalshabet.png 16 | DalShabet/bestimage.jpeg 17 | dalshabet/serri_only.png 18 | 19 | The following files would not be considered: 20 | 21 | Dal★Shabet.png (non-alphanumeric character) 22 | Dalshabet.gif (wrong capitalization) 23 | DalShabet.webm (wrong extension) 24 | DalShabet/SomeoneLikeU_MV.mp4 (wrong extension) 25 | Dal★Shabet/party.png (non-alphanumeric character in folder) 26 | 27 | 28 | 29 | The same concept applies to tracks, however their name consists of all artists 30 | joined by a dash (-), then an underscore (_) and the track title. Artists should 31 | be joined in alphabetical order, but filenames with the wrong order might still 32 | work for a low artist number. Rules as above apply, so if your track was named 33 | "Epic Crossover 파티 협동" and it was performed by HyunA, Dal★Shabet and Taylor 34 | Swift, the following files would be considered: 35 | 36 | DalShabet-HyunA-TaylorSwift_EpicCrossover.jpg 37 | DalShabet-TaylorSwift-HyunA_EpicCrossover파티협동.png 38 | dalshabet-hyuna-taylorswift_epiccrossover/albumcover.png 39 | DalShabet-HyunA-TaylorSwift_EpicCrossover파티협동/dancing.gif 40 | 41 | Not accepted would be: 42 | 43 | DalShabet-HyunA-Taylor Swift_epiccrossover.jpg (wrong capitalization) 44 | -------------------------------------------------------------------------------- /maloja/data_files/state/images/tracks/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/state/images/tracks/dummy -------------------------------------------------------------------------------- /maloja/data_files/state/import/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/data_files/state/import/dummy -------------------------------------------------------------------------------- /maloja/database/associated.py: -------------------------------------------------------------------------------- 1 | ## dealing with loading the associated artists rules into a database 2 | ## right now this is kind of absurd because we're storing it in a db while not 3 | ## actually using its permanence, but this makes it possible to use the information 4 | ## directly in sql 5 | 6 | 7 | import csv 8 | import os 9 | 10 | from . import sqldb 11 | from ..pkg_global.conf import data_dir 12 | 13 | 14 | def load_associated_rules(): 15 | # delete old 16 | with sqldb.engine.begin() as conn: 17 | op = sqldb.DB['associated_artists'].delete().where() 18 | conn.execute(op) 19 | 20 | # load from file 21 | rawrules = [] 22 | try: 23 | for f in os.listdir(data_dir["rules"]()): 24 | if f.split('.')[-1].lower() != 'tsv': continue 25 | filepath = data_dir["rules"](f) 26 | with open(filepath,'r') as filed: 27 | reader = csv.reader(filed,delimiter="\t") 28 | rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')] 29 | except FileNotFoundError: 30 | return 31 | 32 | rules = [{'source_artist':r[1],'target_artist':r[2]} for r in rawrules if r[0]=="countas"] 33 | 34 | #for rule in rules: 35 | # print(f"Rule to replace {rule['source_artist']} with {rule['target_artist']}:") 36 | # test = {k:sqldb.get_artist_id(rule[k],create_new=False) for k in rule} 37 | # if test['source_artist'] is None: print("axed") 38 | 39 | #allartists = set([*[r['source_artist'] for r in rules],*[r['target_artist'] for r in rules]]) 40 | 41 | # find ids 42 | rules = [{k:sqldb.get_artist_id(rule[k],create_new=False) for k in rule} for rule in rules] 43 | rules = [r for r in rules if r['source_artist'] is not None] 44 | 45 | # write to db 46 | ops = [ 47 | sqldb.DB['associated_artists'].insert().values(**r).prefix_with('OR IGNORE') 48 | for r in rules 49 | ] 50 | 51 | with sqldb.engine.begin() as conn: 52 | for op in ops: 53 | conn.execute(op) 54 | -------------------------------------------------------------------------------- /maloja/database/exceptions.py: -------------------------------------------------------------------------------- 1 | from bottle import HTTPError 2 | 3 | 4 | class EntityExists(Exception): 5 | def __init__(self, entitydict): 6 | self.entitydict = entitydict 7 | 8 | 9 | class TrackExists(EntityExists): 10 | pass 11 | 12 | 13 | class ArtistExists(EntityExists): 14 | pass 15 | 16 | 17 | class AlbumExists(EntityExists): 18 | pass 19 | 20 | 21 | # if the scrobbles dont match 22 | class DuplicateTimestamp(Exception): 23 | def __init__(self, existing_scrobble, rejected_scrobble): 24 | self.existing_scrobble = existing_scrobble 25 | self.rejected_scrobble = rejected_scrobble 26 | 27 | 28 | # if it's the same scrobble 29 | class DuplicateScrobble(Exception): 30 | def __init__(self, scrobble): 31 | self.scrobble = scrobble 32 | 33 | 34 | class DatabaseNotBuilt(HTTPError): 35 | def __init__(self): 36 | super().__init__( 37 | status=503, 38 | body="The Maloja Database is being upgraded to support new Maloja features. This could take a while.", 39 | headers={"Retry-After": 120} 40 | ) 41 | 42 | 43 | class MissingScrobbleParameters(Exception): 44 | def __init__(self, params=[]): 45 | self.params = params 46 | 47 | 48 | class MissingEntityParameter(Exception): 49 | pass 50 | 51 | 52 | class EntityDoesNotExist(HTTPError): 53 | entitytype = 'Entity' 54 | 55 | def __init__(self,entitydict): 56 | self.entitydict = entitydict 57 | super().__init__( 58 | status=404, 59 | body=f"The {self.entitytype} '{self.entitydict}' does not exist in the database." 60 | ) 61 | 62 | 63 | class ArtistDoesNotExist(EntityDoesNotExist): 64 | entitytype = 'Artist' 65 | 66 | 67 | class AlbumDoesNotExist(EntityDoesNotExist): 68 | entitytype = 'Album' 69 | 70 | 71 | class TrackDoesNotExist(EntityDoesNotExist): 72 | entitytype = 'Track' 73 | -------------------------------------------------------------------------------- /maloja/database/jinjaview.py: -------------------------------------------------------------------------------- 1 | from .. import database 2 | from . sqldb import engine 3 | 4 | from .dbcache import serialize 5 | 6 | from ..pkg_global.conf import malojaconfig 7 | 8 | from doreah.logging import log 9 | 10 | 11 | # this is a wrapper object that provides a DB connection so that one jinja page 12 | # (with all its included partials) can use it for all functions 13 | # it also translates the non-unpacked calls to unpacked calls that the DB wants 14 | # it also maintains a request-local cache since many webpages use the same stats 15 | # several times 16 | class JinjaDBConnection: 17 | def __init__(self): 18 | self.cache = {} 19 | self.hits = 0 20 | self.misses = 0 21 | def __enter__(self): 22 | self.conn = engine.connect() 23 | return self 24 | def __exit__(self, exc_type, exc_value, exc_traceback): 25 | self.conn.close() 26 | if malojaconfig['USE_REQUEST_CACHE']: 27 | log(f"Generated page with {self.hits}/{self.hits+self.misses} local Cache hits",module="debug_performance") 28 | del self.cache 29 | def __getattr__(self,name): 30 | originalmethod = getattr(database,name) 31 | 32 | def packedmethod(*keys): 33 | kwargs = {} 34 | for k in keys: 35 | kwargs.update(k) 36 | if malojaconfig['USE_REQUEST_CACHE']: 37 | cachekey = serialize((id(originalmethod),kwargs)) 38 | if cachekey in self.cache: 39 | self.hits += 1 40 | return self.cache[cachekey] 41 | else: 42 | self.misses += 1 43 | result = originalmethod(**kwargs,dbconn=self.conn) 44 | self.cache[cachekey] = result 45 | return result 46 | else: 47 | result = originalmethod(**kwargs,dbconn=self.conn) 48 | return result 49 | 50 | 51 | return packedmethod 52 | -------------------------------------------------------------------------------- /maloja/dev/__init__.py: -------------------------------------------------------------------------------- 1 | ### Subpackage that takes care of all things that concern the server process itself, 2 | ### e.g. analytics 3 | -------------------------------------------------------------------------------- /maloja/dev/apidebug.py: -------------------------------------------------------------------------------- 1 | import bottle, waitress 2 | 3 | from ..pkg_global.conf import malojaconfig 4 | 5 | from doreah.logging import log 6 | from nimrodel import EAPI as API 7 | 8 | 9 | PORT = malojaconfig["PORT"] 10 | HOST = malojaconfig["HOST"] 11 | 12 | the_listener = API(delay=True) 13 | 14 | @the_listener.get("{path}") 15 | @the_listener.post("{path}") 16 | def all_requests(path,**kwargs): 17 | result = { 18 | 'path':path, 19 | 'payload': kwargs 20 | } 21 | log(result) 22 | return result 23 | 24 | 25 | def run(): 26 | server = bottle.Bottle() 27 | the_listener.mount(server,path="apis") 28 | waitress.serve(server, listen=f"*:{PORT}") 29 | -------------------------------------------------------------------------------- /maloja/dev/generate.py: -------------------------------------------------------------------------------- 1 | import random 2 | import datetime 3 | 4 | from doreah.io import ask 5 | 6 | 7 | artists = [ 8 | "Chou Tzuyu","Jennie Kim","Kim Seolhyun","Nancy McDonie","Park Junghwa","Hirai Momo","Rosé Park","Laura Brehm","HyunA", 9 | "Jeremy Soule","Jerry Goldsmith","Howard Shore","Tilman Sillescu","James Newton Howard","Bear McCreary","David Newman", 10 | "Approaching Nirvana","7 Minutes Dead","Tut Tut Child","Mr FijiWiji","Tut Tut Child" 11 | ] 12 | 13 | adjectives = [ 14 | "Black","Pink","Yellow","Scarlet","Purple","Burgundy","Orange","Golden","Green","Vermilion", 15 | "Misty","Foggy","Cloudy","Hazy","Cold","Hot","Warm","Dark","Bright", 16 | "Long","Short","Last","First","Final","Huge","Tiny","Important" 17 | ] 18 | nouns = [ 19 | "Ship","Princess","Castle","Monastery","Sword","War","Battle","Temple","Army","Horse", 20 | "Valley","River","Waterfall","Mountain","Tree","Forest","Sea","Desert","Montains","Clouds","Glacier","Sun","Moon", 21 | "Dusk","Dawn","Twilight","Nightfall","Sunset","Sunrise", 22 | "Penguin","Tiger","Phoenix","Qilin","Dragon","Tortoise","Bird","Toucan", 23 | "Area","Region","Land","Span","Gate","Arch","Country","Field", 24 | "Cherry","Pear","Olive","Apple","Peach","Berry", 25 | "Sapphire","Emerald","Jade","Ruby" 26 | ] 27 | prepositions = ["in","of","over","under","about","across","inside","toward","on","within","with"] 28 | verbs = [ 29 | "Lifting","Stealing","Dancing","Running","Jumping","Singing","Moving","Climbing","Walking","Wandering", 30 | "Fighting","Entering","Leaving","Meeting","Watching","Eating" 31 | ] 32 | 33 | patterns = [ 34 | "{n1} {p1} the {a1} {n2}", # Land of the Golden Sun 35 | "The {a1} {n1}", # The Green Dragon 36 | "{p1} the {a1} {n1}", # Under the Dark Span 37 | "{a1} {n1} {p1} the {a2} {n2}", 38 | "{v1} the {a1} {n1}", 39 | "{v1} {p1} the {a1} {n1}", 40 | "{v1} and {v2}", 41 | "{a1} {n1} {p1} the {n2} {n3}", 42 | "{a1} {n1} and the {n2} {n3}", # Black Horse and the Cherry Tree 43 | "{a1} {a2} {p1} your {n1}", # Black Pink in your Area 44 | "Forward {p1} {n1}" 45 | ] 46 | 47 | def generate_track(): 48 | 49 | title = random.choice(patterns).format( 50 | n1=random.choice(nouns), 51 | n2=random.choice(nouns), 52 | n3=random.choice(nouns), 53 | a1=random.choice(adjectives), 54 | a2=random.choice(adjectives), 55 | p1=random.choice(prepositions), 56 | p2=random.choice(prepositions), 57 | v1=random.choice(verbs), 58 | v2=random.choice(verbs) 59 | ) 60 | title = "".join([title[0].upper(),title[1:]]) 61 | trackartists = [random.choice(artists) for _ in range(random.randint(1, 3))] 62 | 63 | return { 64 | "artists":trackartists, 65 | "title":title 66 | } 67 | 68 | 69 | 70 | def generate_scrobbles(n=200): 71 | 72 | from ..database.sqldb import add_scrobbles 73 | 74 | n = int(n) 75 | 76 | if ask("Generate random scrobbles?",default=False): 77 | scrobbles = [] 78 | for _ in range(n): 79 | track = generate_track() 80 | print("Generated",track) 81 | for _ in range(random.randint(1, 50)): 82 | timestamp = random.randint(1, int(datetime.datetime.now().timestamp())) 83 | 84 | scrobbles.append({ 85 | "time":timestamp, 86 | "track":{ 87 | "artists":track['artists'], 88 | "title":track['title'] 89 | }, 90 | "duration":None, 91 | "origin":"generated" 92 | }) 93 | 94 | add_scrobbles(scrobbles) 95 | -------------------------------------------------------------------------------- /maloja/dev/profiler.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import cProfile, pstats 4 | import time 5 | 6 | from doreah.logging import log 7 | 8 | from ..pkg_global.conf import data_dir 9 | 10 | 11 | 12 | 13 | FULL_PROFILE = False 14 | SINGLE_CALLS = False 15 | # only save the last single call instead of adding up all calls 16 | # of that function for more representative performance result 17 | 18 | if not SINGLE_CALLS: 19 | profilers = {} 20 | times = {} 21 | 22 | def profile(func): 23 | 24 | realfunc = func 25 | while hasattr(realfunc, '__innerfunc__'): 26 | realfunc = realfunc.__innerfunc__ 27 | 28 | def newfunc(*args,**kwargs): 29 | 30 | starttime = time.time() 31 | 32 | if FULL_PROFILE: 33 | benchmarkfolder = data_dir['logs']("benchmarks") 34 | os.makedirs(benchmarkfolder,exist_ok=True) 35 | if SINGLE_CALLS: 36 | localprofiler = cProfile.Profile() 37 | else: 38 | localprofiler = profilers.setdefault(realfunc,cProfile.Profile()) 39 | localprofiler.enable() 40 | 41 | result = func(*args,**kwargs) 42 | 43 | if FULL_PROFILE: 44 | localprofiler.disable() 45 | 46 | seconds = time.time() - starttime 47 | 48 | if not SINGLE_CALLS: 49 | times.setdefault(realfunc,[]).append(seconds) 50 | 51 | if SINGLE_CALLS: 52 | log(f"Executed {realfunc.__name__} ({args}, {kwargs}) in {seconds:.3f}s",module="debug_performance") 53 | else: 54 | log(f"Executed {realfunc.__name__} ({args}, {kwargs}) in {seconds:.3f}s (Average: { sum(times[realfunc])/len(times[realfunc]):.3f}s)",module="debug_performance") 55 | 56 | if FULL_PROFILE: 57 | targetfilename = os.path.join(benchmarkfolder,f"{realfunc.__name__}.stats") 58 | try: 59 | pstats.Stats(localprofiler).dump_stats(targetfilename) 60 | log(f"Saved benchmark as {targetfilename}") 61 | except Exception: 62 | log(f"Failed to save benchmark as {targetfilename}") 63 | 64 | return result 65 | 66 | return newfunc 67 | -------------------------------------------------------------------------------- /maloja/jinjaenv/filters.py: -------------------------------------------------------------------------------- 1 | def fixlength(real,target): 2 | t = real[:target] 3 | while len(t) 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 |

Maloja

14 |
15 | Version {{ pkginfo.VERSION }}
16 | {# {% if adminmode %} 17 | Python {{ platform.sys.version }}
18 | {{ platform.system() }} {{ platform.release() }} ({{ platform.machine() }})
19 |
20 | {% set pid = psutil.os.getpid() %} 21 | {% set proc = psutil.Process(pid) %} 22 | CPU: 23 | {{ proc.cpu_percent() | int }}% Maloja, 24 | {{ (psutil.getloadavg()[2]/psutil.os.cpu_count() * 100) | int }}% System 25 |
26 | RAM: 27 | {{ (proc.memory_info().rss / (1024*1024)) | int }}MB Maloja ({{ proc.memory_percent() | int }}%), 28 | {{ (psutil.virtual_memory().used / (1024*1024)) | int }}MB System ({{ psutil.virtual_memory().percent | int }}%) 29 | {% endif %} #} 30 | 31 |

32 | 33 |

34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Get your own Maloja server: 42 |

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |

53 | Maloja is released under the GNU General Public License v3.0. 54 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /maloja/web/jinja/abstracts/admin.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - Backend{% endblock %} 3 | 4 | 5 | {% block content %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 43 | 44 |
13 |
14 |
16 |

Admin Panel

17 |
18 | 19 | {% for tab_url,tab_name in [ 20 | ['overview','Overview'], 21 | ['setup','Server Setup'], 22 | ['settings','Settings'], 23 | ['apikeys','API Keys'], 24 | ['manual','Manual Scrobbling'], 25 | ['albumless','Tracks without Albums'] 26 | 27 | ] %} 28 | {# ['import','Scrobble Import'], 29 | ['issues','Database Maintenance'] 30 | hide for now #} 31 | {% if page=='admin_' + tab_url %} 32 | {{ tab_name }} 33 | {% else %} 34 | {{ tab_name }} 35 | {% endif %} {%- if not loop.last %}|{% endif %} 36 | {% endfor %} 37 | 38 | 39 | 40 |

41 | 42 |
45 | 46 | {% block maincontent %} 47 | {% endblock %} 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /maloja/web/jinja/admin_albumless.jinja: -------------------------------------------------------------------------------- 1 | {% set page ='admin_albumless' %} 2 | {% extends "abstracts/admin.jinja" %} 3 | {% block title %}Maloja - Albumless Tracks{% endblock %} 4 | 5 | {% block maincontent %} 6 | Here you can find tracks that currently have no album.

7 | 8 | {% with list = dbc.get_tracks_without_album() %} 9 | You have {{list|length}} tracks with no album.

10 | 11 | {% include 'partials/list_tracks.jinja' %} 12 | {% endwith %} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /maloja/web/jinja/admin_apikeys.jinja: -------------------------------------------------------------------------------- 1 | {% set page ='admin_apikeys' %} 2 | {% extends "abstracts/admin.jinja" %} 3 | {% block title %}Maloja - API Keys{% endblock %} 4 | 5 | 6 | {% block maincontent %} 7 | Here you can add and remove API keys used by your scrobblers. It is recommended to use 8 | a different key for each scrobbler.

9 | {{ apikeys.html() }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /maloja/web/jinja/admin_import.jinja: -------------------------------------------------------------------------------- 1 | {% set page ='admin_import' %} 2 | {% extends "abstracts/admin.jinja" %} 3 | {% block title %}Maloja - Import Scrobbles{% endblock %} 4 | 5 | 6 | {% block scripts %} 7 | 13 | {% endblock %} 14 | 15 | 16 | {% block maincontent %} 17 | You can import your scrobbles from other platforms. This will not overwrite scrobbles you've already made, 18 | unless they are deemed to be equivalent (exact same timestamp). Importing multiple times from the same 19 | source should also not lead to any duplicates.

20 | 21 | {% for importsource in thirdparty.services.import %} 22 |

{{ importsource.name }}

23 | 24 | 25 | 26 | {% endfor %} 27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /maloja/web/jinja/admin_issues.jinja: -------------------------------------------------------------------------------- 1 | {% set page ='admin_issues' %} 2 | {% extends "abstracts/admin.jinja" %} 3 | {% block title %}Maloja - Issues{% endblock %} 4 | 5 | {% import 'snippets/links.jinja' as links %} 6 | 7 | {% block scripts %} 8 | 34 | {% endblock %} 35 | 36 | {% set issuedata = dbc.issues() %} 37 | 38 | {% block maincontent %} 39 | 40 | 41 |

Maloja can identify possible problems with consistency or redundancy in your library. After making any changes, you should rebuild your library.

42 | 43 | 44 | 45 | {% if issuedata.inconsistent %} 46 | 47 | 48 | 49 | 50 | {% endif %} 51 | 52 | {% for issue in issuedata.duplicates %} 53 | 54 | 55 | 58 | 61 | 62 | {% endfor %} 63 | 64 | {% for issue in issuedata.combined %} 65 | 66 | 69 | 72 | 73 | {% endfor %} 74 | 75 | {% for issue in issuedata.newartists %} 76 | 77 | 78 | 81 | 82 | {% endfor %} 83 | 84 |
The current database wasn't built with all current rules in effect. Any problem below might be a false alarm and fixing it could create redundant rules.
{{ links.link(issue[0]) }} is a possible duplicate of {{ links.link(issue[1]) }} 56 | 57 | 59 | 60 |
{{ links.link(issue[0]) }} sounds like the combination of {{ issue[1].__len__() }} artists: 67 | {{ issue[1]|join(", ") }} 68 | 70 | 71 |
Is '{{ issue[0] }}' in '{{ links.link(issue[1]) }}' an artist? 79 | 80 |
85 | 86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /maloja/web/jinja/admin_manual.jinja: -------------------------------------------------------------------------------- 1 | {% set page ='admin_manual' %} 2 | {% extends "abstracts/admin.jinja" %} 3 | {% block title %}Maloja - Manual Scrobbling{% endblock %} 4 | 5 | {% block scripts %} 6 | 7 | 15 | {% endblock %} 16 | 17 | 18 | {% block maincontent %} 19 | 20 |

Scrobble new discovery

21 | 22 | 23 | 24 | 29 | 30 | 31 | 36 | 37 | 38 | 43 | 44 | 45 | 50 | 51 | 52 | 56 | 59 | 60 | 61 |
25 | Artists: 26 | 27 | 28 |
32 | Title: 33 | 34 | 35 |
39 | Album artists (Optional): 40 | 41 | 42 |
46 | Album (Optional): 47 | 48 | 49 |
53 | 54 | Custom Time: 55 | 57 | 58 |
62 | 63 | 68 | 69 |
70 | 71 | 72 | Use track artists as album artists fallback 73 | 74 |

75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |

Search

84 | 85 | 86 |

87 |
88 | 89 | 90 | {% endblock %} 91 | -------------------------------------------------------------------------------- /maloja/web/jinja/admin_settings.jinja: -------------------------------------------------------------------------------- 1 | {% set page ='admin_settings' %} 2 | {% extends "abstracts/admin.jinja" %} 3 | {% block title %}Maloja - Settings{% endblock %} 4 | 5 | 6 | {% block maincontent %} 7 | {{ settings.html() }} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /maloja/web/jinja/charts_albums.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - Album Charts{% endblock %} 3 | 4 | {% import 'snippets/links.jinja' as links %} 5 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 6 | 7 | {% block scripts %} 8 | 9 | {% endblock %} 10 | 11 | {% set charts = dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':False}) %} 12 | {% set pages = math.ceil(charts.__len__() / amountkeys.perpage) %} 13 | {% if charts[0] is defined %} 14 | {% set topalbum = charts[0].album %} 15 | {% set img = images.get_album_image(topalbum) %} 16 | {% else %} 17 | {% set img = "/favicon.png" %} 18 | {% endif %} 19 | 20 | 21 | {% block content %} 22 | 23 | 24 | 25 | 28 | 37 | 38 |
26 |
27 |
29 |

Album Charts

View #1 Albums
30 | {{ filterdesc.desc(filterkeys,limitkeys) }} 31 |

32 | {% with delimitkeys = {} %} 33 | {% include 'snippets/timeselection.jinja' %} 34 | {% endwith %} 35 | 36 |
39 | 40 | {% if settings['CHARTS_DISPLAY_TILES'] %} 41 | {% include 'partials/charts_albums_tiles.jinja' %} 42 |

43 | {% endif %} 44 | 45 | {% with compare=true %} 46 | {% include 'partials/charts_albums.jinja' %} 47 | {% endwith %} 48 | 49 | {% import 'snippets/pagination.jinja' as pagination %} 50 | {{ pagination.pagination(filterkeys,limitkeys,delimitkeys,amountkeys,pages) }} 51 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /maloja/web/jinja/charts_artists.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - Artist Charts{% endblock %} 3 | 4 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 5 | 6 | {% block scripts %} 7 | 8 | {% endblock %} 9 | 10 | {% set charts = dbc.get_charts_artists(filterkeys,limitkeys,specialkeys) %} 11 | 12 | 13 | 14 | {% set pages = math.ceil(charts.__len__() / amountkeys.perpage) %} 15 | {% if charts[0] is defined %} 16 | {% set topartist = charts[0].artist %} 17 | {% set img = images.get_artist_image(topartist) %} 18 | {% else %} 19 | {% set img = "/favicon.png" %} 20 | {% endif %} 21 | 22 | 23 | 24 | {% block content %} 25 | 26 | 27 | 28 | 31 | 40 | 41 |
29 |
30 |
32 |

Artist Charts

View #1 Artists
33 | {{ filterdesc.desc(filterkeys,limitkeys) }} 34 |

35 | {% with delimitkeys = {}, artistchart=True %} 36 | {% include 'snippets/timeselection.jinja' %} 37 | {% endwith %} 38 | 39 |
42 | 43 | {% if settings['CHARTS_DISPLAY_TILES'] %} 44 | {% include 'partials/charts_artists_tiles.jinja' %} 45 |

46 | {% endif %} 47 | 48 | {% with compare=true %} 49 | {% include 'partials/charts_artists.jinja' %} 50 | {% endwith %} 51 | 52 | {% import 'snippets/pagination.jinja' as pagination %} 53 | {{ pagination.pagination(filterkeys,limitkeys,delimitkeys,amountkeys,pages) }} 54 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /maloja/web/jinja/charts_tracks.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - Track Charts{% endblock %} 3 | 4 | {% import 'snippets/links.jinja' as links %} 5 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 6 | 7 | {% block scripts %} 8 | 9 | {% endblock %} 10 | 11 | {% set charts = dbc.get_charts_tracks(filterkeys,limitkeys) %} 12 | {% set pages = math.ceil(charts.__len__() / amountkeys.perpage) %} 13 | {% if charts[0] is defined %} 14 | {% set toptrack = charts[0].track %} 15 | {% set img = images.get_track_image(toptrack) %} 16 | {% else %} 17 | {% set img = "/favicon.png" %} 18 | {% endif %} 19 | 20 | 21 | {% block content %} 22 | 23 | 24 | 25 | 28 | 37 | 38 |
26 |
27 |
29 |

Track Charts

View #1 Tracks
30 | {{ filterdesc.desc(filterkeys,limitkeys) }} 31 |

32 | {% with delimitkeys = {} %} 33 | {% include 'snippets/timeselection.jinja' %} 34 | {% endwith %} 35 | 36 |
39 | 40 | {% if settings['CHARTS_DISPLAY_TILES'] %} 41 | {% include 'partials/charts_tracks_tiles.jinja' %} 42 |

43 | {% endif %} 44 | 45 | {% with compare=true %} 46 | {% include 'partials/charts_tracks.jinja' %} 47 | {% endwith %} 48 | 49 | {% import 'snippets/pagination.jinja' as pagination %} 50 | {{ pagination.pagination(filterkeys,limitkeys,delimitkeys,amountkeys,pages) }} 51 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /maloja/web/jinja/error.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - Error{% endblock %} 3 | 4 | {% block content %} 5 | 6 | 7 | 10 | 15 | 16 |
8 |
9 |
11 |

{{ error_desc | e }}


12 | {{ error_full_desc | e }} 13 | 14 |
17 | 18 | 19 | Kazuma, Aqua, Darkness and Megumin have been dispatched to deal with this situation.

20 | {% if adminmode %} 21 | You are only seeing this information because you are in admin mode: 22 |

23 |
{% autoescape true -%} 24 | {{ traceback }} 25 | {%- endautoescape %}
26 |

27 | If you think this shouldn't have happened, consider 28 | opening an issue 29 | describing the situation. 30 | {% endif %} 31 | 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/LICENSE-octicons: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 GitHub Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/add_album.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/add_album_confirm.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/add_artist.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/add_artist_confirm.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/association_cancel.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/association_mark.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/association_unmark.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/cert_album.jinja: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 10 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/cert_track.jinja: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 12 | 15 | 21 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/delete.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/disassociate.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/edit.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/merge.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/merge_cancel.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/merge_mark.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/merge_unmark.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/nodata.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
No scrobbles yet! 7 |
8 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/remove_album.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/remove_artist.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/reparse.jinja: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /maloja/web/jinja/icons/settings.jinja: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/album_showcase.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | 4 |
5 | 6 | 7 | {% for entry in dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':True}) %} 8 | 9 | {%- set cert = None -%} 10 | {%- if entry.scrobbles >= settings.scrobbles_gold_album -%}{% set cert = 'gold' %}{%- endif -%} 11 | {%- if entry.scrobbles >= settings.scrobbles_platinum_album -%}{% set cert = 'platinum' %}{%- endif -%} 12 | {%- if entry.scrobbles >= settings.scrobbles_diamond_album -%}{% set cert = 'diamond' %}{%- endif -%} 13 | 14 | 15 | 16 | 21 | 25 | 26 |
 
17 | 18 |
19 |
20 |
22 | {{ links.links(entry.album.artists) }}
23 | {{ links.link(entry.album) }} 24 |
27 | {% endfor %} 28 | 29 | {% for entry in dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':False}) %} 30 | 31 | 32 | {% if info.artist not in (entry.album.artists or []) %} 33 | 34 | {%- set cert = None -%} 35 | {%- if entry.scrobbles >= settings.scrobbles_gold_album -%}{% set cert = 'gold' %}{%- endif -%} 36 | {%- if entry.scrobbles >= settings.scrobbles_platinum_album -%}{% set cert = 'platinum' %}{%- endif -%} 37 | {%- if entry.scrobbles >= settings.scrobbles_diamond_album -%}{% set cert = 'diamond' %}{%- endif -%} 38 | 39 | 40 | 41 | 46 | 50 | 51 |
Appears on
42 | 43 |
44 |
45 |
47 | {{ links.links(entry.album.artists) }}
48 | {{ links.link(entry.album) }} 49 |
52 | 53 | {% endif %} 54 | {% endfor %} 55 | 56 |
57 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/awards_album.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | {% macro medals(info) %} 4 | 5 | 6 | {% for year in info.medals.gold -%} 7 | 8 | {{ year }} 9 | 10 | {%- endfor %} 11 | {% for year in info.medals.silver -%} 12 | 13 | {{ year }} 14 | 15 | {%- endfor %} 16 | {% for year in info.medals.bronze -%} 17 | 18 | {{ year }} 19 | 20 | {%- endfor %} 21 | 22 | {% if info.medals.gold or info.medals.silver or info.medals.bronze %} 23 | 24 | {% endif %} 25 | 26 | {%- endmacro %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% macro topweeks(info) %} 36 | 37 | {% set encodedalbum = mlj_uri.uriencode({'album':info.album}) %} 38 | 39 | 40 | 41 | {% if info.topweeks > 0 -%} 42 | 43 | {{ info.topweeks }} 44 | {%- endif %} 45 | 46 | 47 | {% if info.topweeks > 0 %} 48 | 49 | {% endif %} 50 | 51 | {%- endmacro %} 52 | 53 | 54 | 55 | {% macro subcerts(album) %} 56 | 57 | 58 | 59 | {% set charts = dbc.get_charts_tracks({'album':album.album,'timerange':malojatime.alltime()}) %} 60 | {% for e in charts -%} 61 | {%- if e.scrobbles >= settings.scrobbles_gold -%}{% set cert = 'gold' %}{%- endif -%} 62 | {%- if e.scrobbles >= settings.scrobbles_platinum -%}{% set cert = 'platinum' %}{%- endif -%} 63 | {%- if e.scrobbles >= settings.scrobbles_diamond -%}{% set cert = 'diamond' %}{%- endif -%} 64 | 65 | {%- if cert -%} 66 | 67 | {% include 'icons/cert_track.jinja' %} 68 | 69 | {%- endif %} 70 | 71 | {%- endfor %} 72 | 73 | {%- endmacro %} -------------------------------------------------------------------------------- /maloja/web/jinja/partials/awards_artist.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | 4 | {% macro medals(info) %} 5 | 6 | 7 | {% for year in info.medals.gold -%} 8 | 9 | {{ year }} 10 | 11 | {%- endfor %} 12 | {% for year in info.medals.silver -%} 13 | 14 | {{ year }} 15 | 16 | {%- endfor %} 17 | {% for year in info.medals.bronze -%} 18 | 19 | {{ year }} 20 | 21 | {%- endfor %} 22 | 23 | {% if info.medals.gold or info.medals.silver or info.medals.bronze %} 24 | 25 | {% endif %} 26 | 27 | {%- endmacro %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% macro topweeks(info) %} 37 | 38 | {% set encodedartist = mlj_uri.uriencode({'artist':info.artist}) %} 39 | 40 | 41 | 42 | 43 | {% if info.topweeks > 0 -%} 44 | 45 | {{ info.topweeks }} 46 | {%- endif %} 47 | 48 | 49 | {% if info.topweeks > 0 %} 50 | 51 | {% endif %} 52 | 53 | {%- endmacro %} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {% macro subcerts(artist) %} 64 | 65 | 66 | 67 | 68 | {% set albumcharts = dbc.get_charts_albums({'artist':artist,'timerange':malojatime.alltime(),'resolve_ids':True,'only_own_albums':True}) %} 69 | {% for e in albumcharts -%} 70 | {%- if e.scrobbles >= settings.scrobbles_gold_album -%}{% set cert = 'gold' %}{%- endif -%} 71 | {%- if e.scrobbles >= settings.scrobbles_platinum_album -%}{% set cert = 'platinum' %}{%- endif -%} 72 | {%- if e.scrobbles >= settings.scrobbles_diamond_album -%}{% set cert = 'diamond' %}{%- endif -%} 73 | 74 | {%- if cert -%} 75 | 76 | {% include 'icons/cert_album.jinja' %} 77 | 78 | {%- endif %} 79 | 80 | {%- endfor %} 81 | 82 | 83 | {% set charts = dbc.get_charts_tracks({'artist':artist,'timerange':malojatime.alltime()}) %} 84 | {% for e in charts -%} 85 | {%- if e.scrobbles >= settings.scrobbles_gold -%}{% set cert = 'gold' %}{%- endif -%} 86 | {%- if e.scrobbles >= settings.scrobbles_platinum -%}{% set cert = 'platinum' %}{%- endif -%} 87 | {%- if e.scrobbles >= settings.scrobbles_diamond -%}{% set cert = 'diamond' %}{%- endif -%} 88 | 89 | {%- if cert -%} 90 | 91 | {% include 'icons/cert_track.jinja' %} 92 | 93 | {%- endif %} 94 | 95 | {%- endfor %} 96 | 97 | 98 | 99 | {%- endmacro %} 100 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/awards_track.jinja: -------------------------------------------------------------------------------- 1 | {% macro medals(info) %} 2 | 3 | 4 | {% for year in info.medals.gold -%} 5 | 6 | {{ year }} 7 | 8 | {%- endfor %} 9 | {% for year in info.medals.silver -%} 10 | 11 | {{ year }} 12 | 13 | {%- endfor %} 14 | {% for year in info.medals.bronze -%} 15 | 16 | {{ year }} 17 | 18 | {%- endfor %} 19 | 20 | {% if info.medals.gold or info.medals.silver or info.medals.bronze %} 21 | 22 | {% endif %} 23 | 24 | {%- endmacro %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% macro topweeks(info) %} 34 | 35 | {% set encodedtrack = mlj_uri.uriencode({'track':info.track}) %} 36 | 37 | 38 | 39 | {% if info.topweeks > 0 %} 40 | 41 | {{ info.topweeks }} 42 | 43 | {% endif %} 44 | 45 | 46 | {% if info.topweeks > 0 %} 47 | 48 | {% endif %} 49 | 50 | {%- endmacro %} 51 | 52 | 53 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/charts_albums.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'snippets/entityrow.jinja' as entityrow %} 3 | 4 | {% if charts is undefined %} 5 | {% set charts = dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':False}) %} 6 | {% endif %} 7 | {% if compare %} 8 | {% if compare is true %} 9 | {% set compare = limitkeys.timerange.next(step=-1) %} 10 | {% if compare is none %}{% set compare = False %}{% endif %} 11 | {% endif %} 12 | {% if compare %} 13 | {% set prevalbums = dbc.get_charts_albums(filterkeys,{'timerange':compare}) %} 14 | 15 | {% set lastranks = {} %} 16 | {% for t in prevalbums %} 17 | {% if lastranks.update({"|".join(t.album.artists or [])+"||"+t.album.albumtitle:t.rank}) %}{% endif %} 18 | {% endfor %} 19 | 20 | {% for t in charts %} 21 | {% if "|".join(t.album.artists or [])+"||"+t.album.albumtitle in lastranks %} 22 | {% if t.update({'last_rank':lastranks["|".join(t.album.artists or [])+"||"+t.album.albumtitle]}) %}{% endif %} 23 | {% endif %} 24 | {% endfor %} 25 | {% endif %} 26 | {% endif %} 27 | 28 | {% set firstindex = amountkeys.page * amountkeys.perpage %} 29 | {% set lastindex = firstindex + amountkeys.perpage %} 30 | 31 | 32 | {% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %} 33 | 34 | {% for e in charts %} 35 | {% if loop.index0 >= firstindex and loop.index0 < lastindex %} 36 | 37 | 38 | 39 | 40 | {% if compare %} 41 | {% if e.last_rank is undefined %} 42 | {% elif e.last_rank < e.rank %} 43 | {% elif e.last_rank > e.rank %} 44 | {% elif e.last_rank == e.rank %} 45 | {% endif %} 46 | {% endif %} 47 | 48 | 49 | {{ entityrow.row(e['album'],adminmode=adminmode) }} 50 | 51 | 52 | 53 | 54 | 55 | {% endif %} 56 | {% endfor %} 57 |
{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}🆕{{ links.link_scrobbles([{'album':e.album,'timerange':limitkeys.timerange}],amount=e['scrobbles']) }}{{ links.link_scrobbles([{'album':e.album,'timerange':limitkeys.timerange}],percent=e['scrobbles']*100/maxbar) }}
58 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/charts_albums_tiles.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | 4 | {% if charts is undefined %} 5 | {% set charts = dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':True}) %} 6 | {% endif %} 7 | 8 | {% set charts_14 = charts | fixlength(14) %} 9 | 10 | 11 |
12 | {% if charts_14[0] is none %} 13 | {% include 'icons/nodata.jinja' %} 14 | {% endif %} 15 | {% for entry in charts_14 %} 16 | {% if entry is not none %} 17 | {% set album = entry.album %} 18 | {% set rank = entry.rank %} 19 | {% set scrobbles = entry.scrobbles %} 20 | 30 | {% else %} 31 |
32 | {% endif %} 33 | {% endfor %} 34 | 35 |
36 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/charts_artists.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'snippets/entityrow.jinja' as entityrow %} 3 | 4 | {% if charts is undefined %} 5 | {% set charts = dbc.get_charts_artists(limitkeys,specialkeys) %} 6 | {% endif %} 7 | 8 | {% if compare %} 9 | {% if compare is true %} 10 | {% set compare = limitkeys.timerange.next(step=-1) %} 11 | {% if compare is none %}{% set compare = False %}{% endif %} 12 | {% endif %} 13 | {% if compare %} 14 | {% set prevartists = dbc.get_charts_artists({'timerange':compare},specialkeys) %} 15 | 16 | {% set lastranks = {} %} 17 | {% for a in prevartists %} 18 | {% if lastranks.update({a.artist:a.rank}) %}{% endif %} 19 | {% endfor %} 20 | 21 | {% for a in charts %} 22 | {% if a.artist in lastranks %} 23 | {% if a.update({'last_rank':lastranks[a.artist]}) %}{% endif %} 24 | {% endif %} 25 | {% endfor %} 26 | {% endif %} 27 | {% endif %} 28 | 29 | {% set firstindex = amountkeys.page * amountkeys.perpage %} 30 | {% set lastindex = firstindex + amountkeys.perpage %} 31 | 32 | 33 | 34 | {% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %} 35 | 36 | 37 | {% for e in charts %} 38 | {% if loop.index0 >= firstindex and loop.index0 < lastindex %} 39 | 40 | 41 | 42 | 43 | {% if compare %} 44 | {% if e.last_rank is undefined %} 45 | {% elif e.last_rank < e.rank %} 46 | {% elif e.last_rank > e.rank %} 47 | {% elif e.last_rank == e.rank %} 48 | {% endif %} 49 | {% endif %} 50 | 51 | 52 | {{ entityrow.row(e['artist'],adminmode=adminmode,counting=([] if specialkeys.separate else e.associated_artists)) }} 53 | 54 | 55 | 56 | 62 | 63 | {% endif %} 64 | {% endfor %} 65 |
{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}🆕{{ links.link_scrobbles([{'artist':e['artist'],'associated':(not specialkeys.separate),'timerange':limitkeys.timerange}],amount=e['scrobbles']) }} 57 | {{ links.link_scrobbles([{'artist':e['artist'],'associated':False,'timerange':limitkeys.timerange}],percent=e['real_scrobbles']*100/maxbar) }} 58 | {%- if e['real_scrobbles'] != e['scrobbles'] -%} 59 | {{ links.link_scrobbles([{'artist':e['artist'],'associated':True,'timerange':limitkeys.timerange}],percent=(e['scrobbles']-e['real_scrobbles'])*100/maxbar) }} 60 | {%- endif %} 61 |
66 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/charts_artists_tiles.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | 4 | {% if charts is undefined %} 5 | {% set charts = dbc.get_charts_artists(limitkeys) %} 6 | {% endif %} 7 | 8 | {% set charts_14 = charts | fixlength(14) %} 9 | 10 | 11 |
12 | {% if charts_14[0] is none %} 13 | {% include 'icons/nodata.jinja' %} 14 | {% endif %} 15 | {% for entry in charts_14 %} 16 | {% if entry is not none %} 17 | {% set artist = entry.artist %} 18 | {% set rank = entry.rank %} 19 | {% set scrobbles = entry.scrobbles %} 20 | 30 | {% else %} 31 |
32 | {% endif %} 33 | {% endfor %} 34 | 35 |
36 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/charts_tracks.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'snippets/entityrow.jinja' as entityrow %} 3 | 4 | {% if charts is undefined %} 5 | {% set charts = dbc.get_charts_tracks(filterkeys,limitkeys) %} 6 | {% endif %} 7 | {% if compare %} 8 | {% if compare is true %} 9 | {% set compare = limitkeys.timerange.next(step=-1) %} 10 | {% if compare is none %}{% set compare = False %}{% endif %} 11 | {% endif %} 12 | {% if compare %} 13 | {% set prevtracks = dbc.get_charts_tracks(filterkeys,{'timerange':compare}) %} 14 | 15 | {% set lastranks = {} %} 16 | {% for t in prevtracks %} 17 | {% if lastranks.update({"|".join(t.track.artists)+"||"+t.track.title:t.rank}) %}{% endif %} 18 | {% endfor %} 19 | 20 | {% for t in charts %} 21 | {% if "|".join(t.track.artists)+"||"+t.track.title in lastranks %} 22 | {% if t.update({'last_rank':lastranks["|".join(t.track.artists)+"||"+t.track.title]}) %}{% endif %} 23 | {% endif %} 24 | {% endfor %} 25 | {% endif %} 26 | {% endif %} 27 | 28 | {% set firstindex = amountkeys.page * amountkeys.perpage %} 29 | {% set lastindex = firstindex + amountkeys.perpage %} 30 | 31 | 32 | {% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %} 33 | 34 | {% for e in charts %} 35 | {% if loop.index0 >= firstindex and loop.index0 < lastindex %} 36 | 37 | 38 | 39 | 40 | {% if compare %} 41 | {% if e.last_rank is undefined %} 42 | {% elif e.last_rank < e.rank %} 43 | {% elif e.last_rank > e.rank %} 44 | {% elif e.last_rank == e.rank %} 45 | {% endif %} 46 | {% endif %} 47 | 48 | 49 | {{ entityrow.row(e['track'],adminmode=adminmode) }} 50 | 51 | 52 | 53 | 54 | 55 | {% endif %} 56 | {% endfor %} 57 |
{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}🆕{{ links.link_scrobbles([{'track':e.track,'timerange':limitkeys.timerange}],amount=e['scrobbles']) }}{{ links.link_scrobbles([{'track':e.track,'timerange':limitkeys.timerange}],percent=e['scrobbles']*100/maxbar) }}
58 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/charts_tracks_tiles.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | 4 | {% if charts is undefined %} 5 | {% set charts = dbc.get_charts_tracks(filterkeys,limitkeys) %} 6 | {% endif %} 7 | 8 | {% set charts_14 = charts | fixlength(14) %} 9 | 10 | 11 |
12 | {% if charts_14[0] is none %} 13 | {% include 'icons/nodata.jinja' %} 14 | {% endif %} 15 | {% for entry in charts_14 %} 16 | {% if entry is not none %} 17 | {% set track = entry.track %} 18 | {% set rank = entry.rank %} 19 | {% set scrobbles = entry.scrobbles %} 20 | 30 | {% else %} 31 |
32 | {% endif %} 33 | {% endfor %} 34 | 35 |
36 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/info_album.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'partials/awards_album.jinja' as awards with context %} 3 | 4 | {% set album = filterkeys.album %} 5 | {% set info = dbc.album_info({'album':album}) %} 6 | {% set encodedalbum = mlj_uri.uriencode({'album':album}) %} 7 | 8 | 9 | 10 | 11 | 23 | 44 | 45 |
12 | {% if adminmode %} 13 |
18 | {% else %} 19 |
20 |
21 | {% endif %} 22 |
24 | {{ links.links(album.artists) }}
25 | {% if condensed %}{% endif %} 26 |

{{ info.album.albumtitle | e }}

27 | {%- if condensed -%}
{% endif %} 28 | #{{ info.position }} 29 |
30 | 31 |

32 | {{ info['scrobbles'] }} Scrobbles 33 |

34 | 35 | 36 | 37 | 38 | 39 | {{ awards.medals(info) }} 40 | {{ awards.topweeks(info) }} 41 | {{ awards.subcerts(info) }} 42 | 43 |
46 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/info_artist.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'partials/awards_artist.jinja' as awards with context %} 3 | 4 | 5 | {% set artist = filterkeys.artist %} 6 | {% set info = dbc.artist_info({'artist':artist}) %} 7 | {% set encodedartist = mlj_uri.uriencode({'artist':artist}) %} 8 | 9 | {% set credited = info.get('replace') %} 10 | {% set included = info.get('associated') %} 11 | 12 | {% if credited is not none %} 13 | {% set competes = false %} 14 | {% else %} 15 | {% set credited = artist %} 16 | {% set competes = true %} 17 | {% endif %} 18 | 19 | 20 | 21 | 22 | 34 | 61 | 62 |
23 | {% if adminmode %} 24 |
29 | {% else %} 30 |
31 |
32 | {% endif %} 33 |
35 | {% if condensed %}{% endif %} 36 |

{{ info.artist | e }}

37 | {%- if condensed -%}
{% endif %} 38 | {% if competes and info['scrobbles']>0 %}#{{ info.position }}{% endif %} 39 |
40 | {% if competes and included and (not condensed) %} 41 | associated: {{ links.links(included) }} 42 | {% elif not competes %} 43 | Competing under {{ links.link(credited) }} (#{{ info.position }}) 44 | {% endif %} 45 | 46 |

47 | {{ info['scrobbles'] }} Scrobbles 48 |

49 | 50 | 51 | 52 | 53 | {% if competes %} 54 | {{ awards.medals(info) }} 55 | {{ awards.topweeks(info) }} 56 | {% endif %} 57 | {{ awards.subcerts(artist) }} 58 | 59 | 60 |
63 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/info_track.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | {% set track = filterkeys.track %} 4 | {% set info = dbc.track_info({'track':track}) %} 5 | {% set encodedtrack = mlj_uri.uriencode({'track':track}) %} 6 | 7 | {% import 'partials/awards_track.jinja' as awards with context %} 8 | 9 | 10 | 11 | 23 | 48 | 49 |
12 | {% if adminmode %} 13 |
18 | {% else %} 19 |
20 |
21 | {% endif %} 22 |
24 | {{ links.links(track.artists) }}
25 | {% if condensed %}{% endif %} 26 |

{{ info.track.title | e }}

27 | {%- if condensed -%}
{% endif %} 28 | #{{ info.position }} 29 |
30 | {% if info.track.album %} 31 | from {{ links.link(info.track.album) }}
32 | {% endif %} 33 | 34 |

35 | {% if adminmode %}{% endif %} 36 | {{ info['scrobbles'] }} Scrobbles 37 |

38 | 39 | 40 | 41 | 42 | 43 | {{ awards.medals(info) }} 44 | {{ awards.topweeks(info) }} 45 | 46 | 47 |
50 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/list_tracks.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'snippets/entityrow.jinja' as entityrow %} 3 | 4 | 5 | 6 | {% set firstindex = amountkeys.page * amountkeys.perpage %} 7 | {% set lastindex = firstindex + amountkeys.perpage %} 8 | 9 | 10 | 11 | {% for e in list %} 12 | {% if loop.index0 >= firstindex and loop.index0 < lastindex %} 13 | 14 | 15 | 16 | {{ entityrow.row(e['track'],adminmode=adminmode) }} 17 | 18 | 19 | {% endif %} 20 | {% endfor %} 21 |
22 | 23 | 28 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/performance.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | {% set ranges = dbc.get_performance(filterkeys,limitkeys,delimitkeys,specialkeys) %} 4 | 5 | {% set minrank = ranges|map(attribute="rank")|reject("none")|max|default(60) %} 6 | {% set minrank = minrank + 20 %} 7 | {% if minrank < 80 %}{% set minrank = 80 %}{% endif %} 8 | 9 | 10 | {% for t in ranges %} 11 | 12 | {% set thisrange = t.range %} 13 | 14 | 15 | 18 | 22 | 23 | 24 | 25 | 26 | {% endfor %} 27 |
{{ thisrange.desc() }} 16 | {{ links.link_rank(filterkeys,specialkeys,thisrange,rank=t.rank) }} 17 | 19 | {% set prct = ((minrank+1-t.rank)*100/minrank if t.rank is not none else 0) %} 20 | {{ links.link_rank(filterkeys,specialkeys,thisrange,percent=prct,rank=t.rank) }} 21 |
28 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/pulse.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | {% set ranges = dbc.get_pulse(filterkeys,limitkeys,delimitkeys) %} 4 | 5 | {% set maxbar = ranges|map(attribute="scrobbles")|max|default(1) %} 6 | {% if maxbar < 1 %}{% set maxbar = 1 %}{% endif %} 7 | 8 | 9 | {% for t in ranges %} 10 | 11 | {% set thisrange = t.range %} 12 | 13 | 14 | 17 | 27 | 28 | {% endfor %} 29 |
{{ thisrange.desc() }} 15 | {{ links.link_scrobbles([filterkeys,{'timerange':thisrange}],amount=t.scrobbles) }} 16 | 18 | {% if 'artist' in filterkeys and filterkeys.get('associated') %} 19 | {{ links.link_scrobbles([{'artist':filterkeys.artist,'associated':False,'timerange':thisrange}],percent=t.real_scrobbles*100/maxbar) }} 20 | {%- if t.real_scrobbles != t.scrobbles -%} 21 | {{ links.link_scrobbles([{'artist':filterkeys.artist,'associated':True,'timerange':thisrange}],percent=(t.scrobbles-t.real_scrobbles)*100/maxbar) }} 22 | {%- endif %} 23 | {% else %} 24 | {{ links.link_scrobbles([filterkeys,{'timerange':thisrange}],percent=t.scrobbles*100/maxbar) }} 25 | {% endif %} 26 |
30 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/scrobbles.jinja: -------------------------------------------------------------------------------- 1 | {% set scrobbles = dbc.get_scrobbles(filterkeys,limitkeys,amountkeys) %} 2 | 3 | {% set firstindex = amountkeys.page * amountkeys.perpage %} 4 | {% set lastindex = firstindex + amountkeys.perpage %} 5 | 6 | {% import 'snippets/entityrow.jinja' as entityrow %} 7 | 8 | 9 | 10 | {% for s in scrobbles -%} 11 | 12 | 13 | {{ entityrow.row(s.track) }} 14 | {% if adminmode %} 15 | 16 | 41 | {% endif %} 42 | 43 | {% endfor %} 44 |
{{ malojatime.timestamp_desc(s["time"],short=shortTimeDesc) }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% include 'icons/reparse.jinja' %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% include 'icons/delete.jinja' %} 37 | 38 | 39 | 40 |
45 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/top_albums.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'snippets/entityrow.jinja' as entityrow %} 3 | 4 | {% set ranges = dbc.get_top_albums(filterkeys,limitkeys,delimitkeys) %} 5 | 6 | {% set maxbar = ranges|map(attribute="scrobbles")|max|default(1) %} 7 | {% if maxbar < 1 %}{% set maxbar = 1 %}{% endif %} 8 | 9 | 10 | {% for e in ranges %} 11 | 12 | {% set thisrange = e.range %} 13 | {% set album = e.album %} 14 | 15 | 16 | 17 | {% if album is none %} 18 | 19 | 20 | 21 | 22 | {% else %} 23 | {{ entityrow.row(album) }} 24 | 25 | 26 | {% endif %} 27 | 28 | 29 | {% endfor %} 30 |
{{ thisrange.desc() }}
n/a0{{ links.link_scrobbles([{'album':album,'timerange':thisrange}],amount=e.scrobbles) }} {{ links.link_scrobbles([{'album':album,'timerange':thisrange}],percent=e.scrobbles*100/maxbar) }}
31 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/top_artists.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'snippets/entityrow.jinja' as entityrow %} 3 | 4 | {% set ranges = dbc.get_top_artists(limitkeys,delimitkeys,specialkeys) %} 5 | 6 | {% set maxbar = ranges|map(attribute="scrobbles")|max|default(1) %} 7 | {% if maxbar < 1 %}{% set maxbar = 1 %}{% endif %} 8 | 9 | 10 | {% for e in ranges %} 11 | 12 | {% set thisrange = e.range %} 13 | {% set artist = e.artist %} 14 | 15 | 16 | 17 | {% if artist is none %} 18 | 19 | 20 | 21 | 22 | {% else %} 23 | {{ entityrow.row(artist,counting=([] if specialkeys.separate else e.associated_artists)) }} 24 | 25 | 31 | {% endif %} 32 | 33 | 34 | {% endfor %} 35 |
{{ thisrange.desc() }}
n/a0{{ links.link_scrobbles([{'artist':artist,'associated':(not specialkeys.separate),'timerange':thisrange}],amount=e.scrobbles) }} 26 | {{ links.link_scrobbles([{'artist':e['artist'],'associated':False,'timerange':e.range}],percent=e['real_scrobbles']*100/maxbar) }} 27 | {%- if e['real_scrobbles'] != e['scrobbles'] -%} 28 | {{ links.link_scrobbles([{'artist':e['artist'],'associated':True,'timerange':e.range}],percent=(e['scrobbles']-e['real_scrobbles'])*100/maxbar) }} 29 | {%- endif %} 30 |
36 | -------------------------------------------------------------------------------- /maloja/web/jinja/partials/top_tracks.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | {% import 'snippets/entityrow.jinja' as entityrow %} 3 | 4 | {% set ranges = dbc.get_top_tracks(filterkeys,limitkeys,delimitkeys) %} 5 | 6 | {% set maxbar = ranges|map(attribute="scrobbles")|max|default(1) %} 7 | {% if maxbar < 1 %}{% set maxbar = 1 %}{% endif %} 8 | 9 | 10 | {% for e in ranges %} 11 | 12 | {% set thisrange = e.range %} 13 | {% set track = e.track %} 14 | 15 | 16 | 17 | {% if track is none %} 18 | 19 | 20 | 21 | 22 | {% else %} 23 | {{ entityrow.row(track) }} 24 | 25 | 26 | {% endif %} 27 | 28 | 29 | {% endfor %} 30 |
{{ thisrange.desc() }}
n/a0{{ links.link_scrobbles([{'track':track,'timerange':thisrange}],amount=e.scrobbles) }} {{ links.link_scrobbles([{'track':track,'timerange':thisrange}],percent=e.scrobbles*100/maxbar) }}
31 | -------------------------------------------------------------------------------- /maloja/web/jinja/performance.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - {{ malojatime.delimit_desc_p(delimitkeys) }} Performance{% endblock %} 3 | 4 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 5 | 6 | {% if filterkeys.get('track') is not none %} 7 | {% set img = images.get_track_image(filterkeys.track) %} 8 | {% elif filterkeys.get('artist') is not none %} 9 | {% set img = images.get_artist_image(filterkeys.artist) %} 10 | {% else %} 11 | {% set img = "/favicon.png" %} 12 | {% endif %} 13 | 14 | {% block content %} 15 | 16 | 17 | 20 | 33 | 34 |
18 |
19 |
21 |

{{ malojatime.delimit_desc_p(delimitkeys) }} Performance

22 | {% if limitkeys != {} %} 23 | View Pulse 24 | {% endif %} 25 |
26 | {{ filterdesc.desc(filterkeys,limitkeys,prefix='of') }} 27 |

28 | {% with artistchart = (filterkeys.get('artist') is not none) %} 29 | {% include 'snippets/timeselection.jinja' %} 30 | {% endwith %} 31 | 32 |
35 | 36 | 37 | {% include 'partials/performance.jinja' %} 38 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /maloja/web/jinja/pulse.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - {{ malojatime.delimit_desc_p(delimitkeys) }} Pulse{% endblock %} 3 | 4 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 5 | 6 | {% if filterkeys.get('track') is not none %} 7 | {% set img = images.get_track_image(filterkeys.track) %} 8 | {% elif filterkeys.get('artist') is not none %} 9 | {% set img = images.get_artist_image(filterkeys.artist) %} 10 | {% else %} 11 | {% set img = "/favicon.png" %} 12 | {% endif %} 13 | 14 | {% block content %} 15 | 16 | 17 | 20 | 31 | 32 |
18 |
19 |
21 |

{{ malojatime.delimit_desc_p(delimitkeys) }} Pulse

22 | {% if filterkeys != {} %} 23 | View Rankings 24 | {% endif %} 25 |
26 | {{ filterdesc.desc(filterkeys,limitkeys,prefix='of') }} 27 |

28 | {% include 'snippets/timeselection.jinja' %} 29 | 30 |
33 | 34 | 35 | {% include 'partials/pulse.jinja' %} 36 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /maloja/web/jinja/scrobbles.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - Scrobbles{% endblock %} 3 | 4 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 5 | {% import 'snippets/pagination.jinja' as pagination %} 6 | 7 | {% set totalscrobbles = dbc.get_scrobbles_num(filterkeys,limitkeys) %} 8 | {% set scrobbles = dbc.get_scrobbles(filterkeys,limitkeys,amountkeys) %} 9 | {% set pages = math.ceil(totalscrobbles / amountkeys.perpage) %} 10 | 11 | {% if filterkeys.get('track') is not none %} 12 | {% set img = images.get_track_image(filterkeys.track) %} 13 | {% elif filterkeys.get('artist') is not none %} 14 | {% set img = images.get_artist_image(filterkeys.artist) %} 15 | {% elif scrobbles.__len__() > 0 %} 16 | {% set img = images.get_track_image(scrobbles[0].track) %} 17 | {% else %} 18 | {% set img = "/favicon.png" %} 19 | {% endif %} 20 | 21 | {% block content %} 22 | 23 | 24 | 25 | 26 | 29 | 40 | 41 |
27 |
28 |
30 |

Scrobbles


31 | {{ filterdesc.desc(filterkeys,limitkeys) }} 32 |
33 |

{{ totalscrobbles }} Scrobbles

34 |
35 | {% with delimitkeys = {} %} 36 | {% include 'snippets/timeselection.jinja' %} 37 | {% endwith %} 38 | 39 |
42 | 43 | 44 | {% include 'partials/scrobbles.jinja' %} 45 | 46 | {{ pagination.pagination(filterkeys,limitkeys,delimitkeys,amountkeys,pages) }} 47 | 48 | 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /maloja/web/jinja/snippets/entityrow.jinja: -------------------------------------------------------------------------------- 1 | {% macro row(entity,counting=[],adminmode=False) %} 2 | 3 | {% import 'snippets/links.jinja' as links %} 4 | 5 | {% if entity is mapping and 'title' in entity %} 6 | {% set img = images.get_track_image(entity) %} 7 | {% elif entity is mapping and 'albumtitle' in entity %} 8 | {% set img = images.get_album_image(entity) %} 9 | {% else %} 10 | {% set img = images.get_artist_image(entity) %} 11 | {% endif %} 12 | 13 | 14 | {% if settings['DISPLAY_ART_ICONS'] %} 15 |
16 | {% endif %} 17 | 18 | {% if entity is mapping and 'title' in entity %} 19 | {% if settings['TRACK_SEARCH_PROVIDER'] %} 20 | {{ links.link_search(entity) }} 21 | {% endif %} 22 | 23 | {{ links.links(entity.artists, restrict_amount=True) }} – {{ links.link(entity) }} 24 | 25 | {% elif entity is mapping and 'albumtitle' in entity %} 26 | 27 | {{ links.links(entity.artists, restrict_amount=True) }} – {{ links.link(entity) }} 28 | 29 | {% else %} 30 | {{ links.link(entity) }} 31 | {% if counting != [] %} 32 | incl. {{ links.links(counting) }} 33 | {% endif %} 34 | 35 | 36 | {% endif %} 37 | 38 | 39 | {% if adminmode %} 40 | 41 | {% include 'icons/merge_mark.jinja' %} 42 | {% include 'icons/merge_unmark.jinja' %} 43 | {% if (entity is mapping) %} 44 | {% include 'icons/association_mark.jinja' %} 45 | {% include 'icons/association_unmark.jinja' %} 46 | {% endif %} 47 | 48 | {% endif %} 49 | 50 | {% endmacro %} 51 | -------------------------------------------------------------------------------- /maloja/web/jinja/snippets/filterdescription.jinja: -------------------------------------------------------------------------------- 1 | {% import 'snippets/links.jinja' as links %} 2 | 3 | {% macro desc(filterkeys,limitkeys,prefix="by") %} 4 | 5 | {% if filterkeys.get('artist') is not none %} 6 | {{ prefix }} {{ links.link(filterkeys.get('artist')) }}{% if filterkeys.get('associated') %} (and associated artists){% endif %} 7 | {% elif filterkeys.get('track') is not none %} 8 | of {{ links.link(filterkeys.get('track')) }} 9 | by {{ links.links(filterkeys["track"]["artists"]) }} 10 | {% elif filterkeys.get('album') is not none %} 11 | from {{ links.link(filterkeys.get('album')) }} 12 | by {{ links.links(filterkeys["album"]["artists"]) }} 13 | {% endif %} 14 | {{ limitkeys.timerange.desc(prefix=True) }} 15 | 16 | {% endmacro %} 17 | -------------------------------------------------------------------------------- /maloja/web/jinja/snippets/pagination.jinja: -------------------------------------------------------------------------------- 1 | {% macro pagination(filterkeys,limitkeys,delimitkeys,amountkeys,pages=1) %} 2 | 3 | {% set page = amountkeys.page %} 4 | 5 | 6 | 7 | 8 |
9 | 10 | {% if pages > 1 %} 11 | {% if page > 1 %} 12 | 13 | 1 | 14 | {% endif %} 15 | 16 | {% if page > 2 %} 17 | ... | 18 | {% endif %} 19 | 20 | {% if page > 0 %} 21 | 22 | {{ page }} « 23 | {% endif %} 24 | 25 | 26 | {{ page + 1 }} 27 | 28 | 29 | {% if page < pages-1 %} 30 | » 31 | {{ page+2 }} 32 | {% endif %} 33 | 34 | {% if page < pages-3 %} 35 | | ... 36 | {% endif %} 37 | 38 | {% if page < pages-2 %} 39 | | 40 | {{ pages }} 41 | {% endif %} 42 | {% endif %} 43 | 44 |
45 | 46 | {% endmacro %} 47 | -------------------------------------------------------------------------------- /maloja/web/jinja/start.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja{% endblock %} 3 | 4 | {% block scripts %} 5 | 6 | 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block content -%} 12 | 13 |
14 | 15 | 16 | {% for module in ['charts_artists','charts_tracks','charts_albums','pulse','lastscrobbles'] %} 17 |
18 | 19 | 20 | {% include 'startpage_modules/' + module + '.jinja' %} 21 | 22 | 23 | 24 |
25 | {% endfor %} 26 | 27 |
28 | 29 | 30 | {%- endblock %} 31 | -------------------------------------------------------------------------------- /maloja/web/jinja/startpage_modules/charts_albums.jinja: -------------------------------------------------------------------------------- 1 |

Top Albums

2 | 3 | {% for r in xcurrent -%} 4 | 5 | {{ r.localisation }} 6 | 7 | {{ "|" if not loop.last }} 8 | {%- endfor %} 9 | 10 |

11 | 12 | 13 | {% for r in xcurrent -%} 14 | 19 | {%- endfor %} 20 | -------------------------------------------------------------------------------- /maloja/web/jinja/startpage_modules/charts_artists.jinja: -------------------------------------------------------------------------------- 1 |

Top Artists

2 | 3 | {% for r in xcurrent -%} 4 | 5 | {{ r.localisation }} 6 | 7 | {{ "|" if not loop.last }} 8 | {%- endfor %} 9 | 10 |

11 | 12 | 13 | {% for r in xcurrent -%} 14 | 19 | {%- endfor %} 20 | -------------------------------------------------------------------------------- /maloja/web/jinja/startpage_modules/charts_tracks.jinja: -------------------------------------------------------------------------------- 1 |

Top Tracks

2 | 3 | {% for r in xcurrent -%} 4 | 5 | {{ r.localisation }} 6 | 7 | {{ "|" if not loop.last }} 8 | {%- endfor %} 9 | 10 |

11 | 12 | 13 | {% for r in xcurrent -%} 14 | 19 | {%- endfor %} 20 | -------------------------------------------------------------------------------- /maloja/web/jinja/startpage_modules/featured.jinja: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Featured

4 | 5 | {% set featured = dbc.get_featured() %} 6 | 7 | {% set entitytypes = [ 8 | {'identifier':'artist','localisation':"Artist", 'template':"info_artist.jinja", 'filterkeys':{"artist": featured.artist } }, 9 | {'identifier':'track','localisation':"Track", 'template':"info_track.jinja", 'filterkeys':{"track": featured.track } }, 10 | {'identifier':'album','localisation':"Album", 'template':"info_album.jinja", 'filterkeys':{"album": featured.album } } 11 | ] %} 12 | 13 | 14 | {% for t in entitytypes -%} 15 | 16 | {{ t.localisation }} 17 | 18 | {{ "|" if not loop.last }} 19 | {%- endfor %} 20 | 21 |


22 | 23 | 24 | {% for t in entitytypes -%} 25 | 34 | {%- endfor %} 35 | 36 | 37 | 38 | 39 | 66 | -------------------------------------------------------------------------------- /maloja/web/jinja/startpage_modules/lastscrobbles.jinja: -------------------------------------------------------------------------------- 1 |

Last Scrobbles

2 | 3 | {% for range in xcurrent %} 4 | {{ range.localisation }} 5 | {{ dbc.get_scrobbles_num({'timerange':range.range}) }} 6 | {% endfor %} 7 |

8 | 9 | 10 | 11 | 12 | {%- with amountkeys = {"perpage":12,"page":0}, shortTimeDesc=True -%} 13 | {% include 'partials/scrobbles.jinja' %} 14 | {%- endwith -%} 15 | 16 | -------------------------------------------------------------------------------- /maloja/web/jinja/startpage_modules/pulse.jinja: -------------------------------------------------------------------------------- 1 |

Pulse

2 | 3 | {% for range in xranges -%} 4 | 5 | {{ range.localisation }} 6 | 7 | {{ "|" if not loop.last }} 8 | {%- endfor %} 9 |

10 | 11 | {% for range in xranges -%} 12 | 17 | {%- endfor %} 18 | -------------------------------------------------------------------------------- /maloja/web/jinja/top_albums.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - #1 Albums{% endblock %} 3 | 4 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 5 | 6 | 7 | 8 | {% set entries = dbc.get_top_albums(filterkeys,limitkeys,delimitkeys) %} 9 | {% set repr = entries | find_representative('album','scrobbles') %} 10 | {% set img = "/favicon.png" if repr is none else images.get_album_image(repr.album) %} 11 | 12 | 13 | {% block content %} 14 | 15 | 16 | 19 | 26 | 27 |
17 |
18 |
20 |

#1 Albums


21 | {{ filterdesc.desc(filterkeys,limitkeys) }} 22 | 23 |

24 | {% include 'snippets/timeselection.jinja' %} 25 |
28 | 29 | {% include 'partials/top_albums.jinja' %} 30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /maloja/web/jinja/top_artists.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - #1 Artists{% endblock %} 3 | 4 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 5 | 6 | 7 | 8 | {% set entries = dbc.get_top_artists(limitkeys,delimitkeys) %} 9 | {% set repr = entries | find_representative('artist','scrobbles') %} 10 | {% set img = "/favicon.png" if repr is none else images.get_artist_image(repr.artist) %} 11 | 12 | 13 | {% block content %} 14 | 15 | 16 | 19 | 28 | 29 |
17 |
18 |
20 |

#1 Artists


21 | {{ filterdesc.desc(filterkeys,limitkeys) }} 22 | 23 |

24 | {% with artistchart = True %} 25 | {% include 'snippets/timeselection.jinja' %} 26 | {% endwith %} 27 |
30 | 31 | 32 | {% include 'partials/top_artists.jinja' %} 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /maloja/web/jinja/top_tracks.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - #1 Tracks{% endblock %} 3 | 4 | {% import 'snippets/filterdescription.jinja' as filterdesc %} 5 | 6 | 7 | 8 | {% set entries = dbc.get_top_tracks(filterkeys,limitkeys,delimitkeys) %} 9 | {% set repr = entries | find_representative('track','scrobbles') %} 10 | {% set img = "/favicon.png" if repr is none else images.get_track_image(repr.track) %} 11 | 12 | 13 | {% block content %} 14 | 15 | 16 | 19 | 26 | 27 |
17 |
18 |
20 |

#1 Tracks


21 | {{ filterdesc.desc(filterkeys,limitkeys) }} 22 | 23 |

24 | {% include 'snippets/timeselection.jinja' %} 25 |
28 | 29 | {% include 'partials/top_tracks.jinja' %} 30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /maloja/web/jinja/wait.jinja: -------------------------------------------------------------------------------- 1 | {% extends "abstracts/base.jinja" %} 2 | {% block title %}Maloja - Please wait...{% endblock %} 3 | 4 | {% block heading %}Rebuilding the database{% endblock %} 5 | 6 | {% block top_info %} 7 | Please wait... 8 | {% endblock %} 9 | 10 | {% block scripts %} 11 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /maloja/web/static/css/grisonsfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face { 3 | font-family: 'Ubuntu'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url('/static/ttf/Ubuntu-Regular.ttf') format('woff2'); 7 | } 8 | -------------------------------------------------------------------------------- /maloja/web/static/css/startpage.css: -------------------------------------------------------------------------------- 1 | div#startpage { 2 | display: grid; 3 | justify-content: center; 4 | 5 | display: fixed; 6 | grid-column-gap: 25px; 7 | grid-row-gap: 25px; 8 | } 9 | 10 | 11 | @media (min-width: 2201px) { 12 | div#startpage { 13 | grid-template-columns: repeat(6, 14vw); 14 | grid-template-rows: repeat(2, 45vh); 15 | grid-column-gap: 2vw; 16 | 17 | grid-template-areas: 18 | "charts_artists charts_artists charts_tracks charts_tracks charts_albums charts_albums" 19 | "empty1 lastscrobbles lastscrobbles pulse pulse empty2"; 20 | } 21 | } 22 | 23 | @media (min-width: 1401px) and (max-width: 2200px) { 24 | div#startpage { 25 | grid-template-columns: repeat(2, 45vw); 26 | grid-template-rows: repeat(3, 45vh); 27 | 28 | grid-template-areas: 29 | "charts_artists lastscrobbles" 30 | "charts_tracks pulse" 31 | "charts_albums empty"; 32 | } 33 | } 34 | 35 | @media (max-width: 1400px) { 36 | div#startpage { 37 | grid-template-columns: 90vw; 38 | 39 | grid-template-areas: 40 | "charts_artists" 41 | "charts_tracks" 42 | "charts_albums" 43 | "lastscrobbles" 44 | "pulse"; 45 | } 46 | 47 | #start_page_module_featured { 48 | display: none; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /maloja/web/static/css/themes/constantinople.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --base-color: #140c12; 3 | --base-color-dark: #080507; 4 | --base-color-light: #2c1b28; 5 | --base-color-accent: #452a3e; 6 | --base-color-accent-dark: #442b3e; 7 | --base-color-accent-light: #dfcad9; 8 | 9 | --text-color: #d8b700; 10 | --text-color-selected: fadeout(var(--text-color),40%); 11 | --text-color-secondary: #6a7600; 12 | --text-color-tertiary: #474f00; 13 | --text-color-focus: #3D428B; 14 | 15 | --ctrl-element-color-bg: rgba(0,255,255,0.1); 16 | --ctrl-element-color-main: rgba(103,85,0,0.7); 17 | --ctrl-element-color-focus: gold; 18 | 19 | --button-color-bg: var(--text-color); 20 | --button-color-bg-focus: var(--text-color-focus); 21 | --button-color-fg: var(--base-color); 22 | --button-color-fg-focus: var(--base-color); 23 | } 24 | -------------------------------------------------------------------------------- /maloja/web/static/css/themes/kda.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --base-color: #2f3493; 3 | --base-color-dark: #000000; 4 | --base-color-light: #59ebfc; 5 | --base-color-accent: #9821a5; 6 | --base-color-accent-dark: #740C7F; 7 | --base-color-accent-light: #D41BE8; 8 | 9 | --text-color: #e5b48f; 10 | --text-color-selected: fadeout(var(--text-color),40%); 11 | --text-color-secondary: #f9f9f9; 12 | --text-color-tertiary: #f7f7f7; 13 | --text-color-focus: #59ebfc; 14 | 15 | --ctrl-element-color-bg: rgba(0,255,255,0.1); 16 | --ctrl-element-color-main: rgba(103,85,0,0.7); 17 | --ctrl-element-color-focus: gold; 18 | 19 | --button-color-bg: var(--text-color); 20 | --button-color-bg-focus: var(--text-color-focus); 21 | --button-color-fg: var(--base-color); 22 | --button-color-fg-focus: var(--base-color); 23 | } 24 | -------------------------------------------------------------------------------- /maloja/web/static/css/themes/maloja.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/css/themes/maloja.css -------------------------------------------------------------------------------- /maloja/web/static/ico/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ico/favicon.ico -------------------------------------------------------------------------------- /maloja/web/static/ico/favicon_old.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ico/favicon_old.ico -------------------------------------------------------------------------------- /maloja/web/static/js/datechange.js: -------------------------------------------------------------------------------- 1 | function datechange() { 2 | 3 | since = document.getElementById("dateselect_from").value; 4 | to = document.getElementById("dateselect_to").value; 5 | 6 | since = since.split("-").join("/") 7 | to = to.split("-").join("/") 8 | 9 | //url = window.location.href 10 | //var url = document.createElement("a") 11 | //url.href = window.location.href 12 | //console.log(url.search) 13 | 14 | keys = window.location.search.substring(1).split("&") 15 | 16 | 17 | var keydict = {}; 18 | for (var i=0;i ` 11 |
12 | ${info.title}
13 | ${info.body} 14 | 15 |
16 | ` 17 | 18 | function htmlToElement(html) { 19 | template = document.createElement('template'); 20 | html = html.trim(); 21 | template.innerHTML = html; 22 | return template.content.firstChild; 23 | } 24 | 25 | function notify(title,msg,notification_type='info',reload=false) { 26 | info = { 27 | 'title':title, 28 | 'body':msg, 29 | 'notification_type':notification_type 30 | } 31 | 32 | var element = htmlToElement(notification_template(info)); 33 | 34 | document.getElementById('notification_area').append(element); 35 | 36 | setTimeout(function(e){e.remove();},7000,element); 37 | } 38 | 39 | function notifyCallback(request) { 40 | var response = request.response; 41 | var status = request.status; 42 | 43 | if (status == 200) { 44 | if (response.hasOwnProperty('warnings') && response.warnings.length > 0) { 45 | var notification_type = 'warning'; 46 | } 47 | else { 48 | var notification_type = 'info'; 49 | } 50 | 51 | var title = "Success!"; 52 | var msg = response.desc || response; 53 | } 54 | else { 55 | var notification_type = 'error'; 56 | var title = "Error: " + response.error.type; 57 | var msg = response.error.desc || ""; 58 | } 59 | 60 | 61 | notify(title,msg,notification_type); 62 | } 63 | -------------------------------------------------------------------------------- /maloja/web/static/js/statselect.js: -------------------------------------------------------------------------------- 1 | localStorage = window.localStorage; 2 | 3 | function showStats(identifier,unit) { 4 | // Make all modules disappear 5 | var modules = document.getElementsByClassName("stat_module_" + identifier); 6 | for (var i=0;i 2 | -------------------------------------------------------------------------------- /maloja/web/static/svg/placeholder_artist.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /maloja/web/static/svg/placeholder_track.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /maloja/web/static/ttf/Ubuntu-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ttf/Ubuntu-Bold.ttf -------------------------------------------------------------------------------- /maloja/web/static/ttf/Ubuntu-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ttf/Ubuntu-BoldItalic.ttf -------------------------------------------------------------------------------- /maloja/web/static/ttf/Ubuntu-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ttf/Ubuntu-Italic.ttf -------------------------------------------------------------------------------- /maloja/web/static/ttf/Ubuntu-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ttf/Ubuntu-Light.ttf -------------------------------------------------------------------------------- /maloja/web/static/ttf/Ubuntu-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ttf/Ubuntu-LightItalic.ttf -------------------------------------------------------------------------------- /maloja/web/static/ttf/Ubuntu-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ttf/Ubuntu-Medium.ttf -------------------------------------------------------------------------------- /maloja/web/static/ttf/Ubuntu-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ttf/Ubuntu-MediumItalic.ttf -------------------------------------------------------------------------------- /maloja/web/static/ttf/Ubuntu-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/maloja/web/static/ttf/Ubuntu-Regular.ttf -------------------------------------------------------------------------------- /maloja/web/static/txt/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # no linting is currently enforced, this is just for reference 2 | [MASTER] 3 | disable=C0114,C0115,C0116, # docstrings 4 | W0703, # broad exception catching 5 | W1514, # open without encoding 6 | 7 | [VARIABLES] 8 | allow-global-unused-variables=no 9 | 10 | [BASIC] 11 | argument-naming-style=snake_case 12 | attr-naming-style=snake_case 13 | class-naming-style=PascalCase 14 | const-naming-style=UPPER_CASE 15 | function-naming-style=snake_case 16 | variable-naming-style=snake_case 17 | indent-string='\t' 18 | good-names=i,j,k, # loops 19 | x,y, # dimensions 20 | e # exceptions 21 | 22 | max-line-length=200 23 | max-module-lines=1000 24 | 25 | [DESIGN] 26 | 27 | max-args=8 28 | max-attributes=7 29 | max-bool-expr=5 30 | max-branches=12 31 | max-locals=15 32 | max-parents=7 33 | max-public-methods=20 34 | max-returns=6 35 | max-statements=50 36 | min-public-methods=2 37 | 38 | [EXCEPTIONS] 39 | 40 | overgeneral-exceptions=BaseException, 41 | Exception 42 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "malojaserver" 3 | version = "3.2.4" 4 | description = "Self-hosted music scrobble database" 5 | readme = "README.md" 6 | requires-python = "==3.12.*" 7 | license = { file="LICENSE" } 8 | authors = [ { name="Johannes Krattenmacher", email="maloja@dev.krateng.ch" } ] 9 | 10 | urls.repository = "https://github.com/krateng/maloja" 11 | urls.documentation = "https://github.com/krateng/maloja" 12 | urls.homepage = "https://github.com/krateng/maloja" 13 | 14 | keywords = ["scrobbling", "music", "selfhosted", "database", "charts", "statistics"] 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 18 | "Operating System :: OS Independent" 19 | ] 20 | 21 | dependencies = [ 22 | "bottle==0.13.*", 23 | "waitress==3.0.*", 24 | "doreah==2.0.*", 25 | "nimrodel==0.8.*", 26 | "setproctitle==1.3.*", 27 | "jinja2==3.1.*", 28 | "lru-dict==1.3.*", 29 | "psutil==5.9.*", 30 | "sqlalchemy==2.0", 31 | "python-datauri==3.0.*", 32 | "python-magic==0.4.*", 33 | "requests==2.32.*", 34 | "toml==0.10.*", 35 | "PyYAML==6.0.*" 36 | ] 37 | 38 | [project.optional-dependencies] 39 | full = [ 40 | "pyvips==2.2.*" 41 | ] 42 | 43 | [project.scripts] 44 | maloja = "maloja.__main__:main" 45 | 46 | [build-system] 47 | requires = ["flit_core >=3.10,<4"] 48 | build-backend = "flit_core.buildapi" 49 | 50 | [tool.flit.module] 51 | name = "maloja" 52 | 53 | [tool.osreqs.alpine] 54 | build =[ 55 | "gcc", 56 | "g++", 57 | "python3-dev", 58 | "libxml2-dev", 59 | "libxslt-dev", 60 | "libffi-dev", 61 | "libc-dev", 62 | "py3-pip", 63 | "linux-headers" 64 | ] 65 | run = [ 66 | "python3", 67 | "py3-lxml", 68 | "tzdata", 69 | "libmagic" 70 | ] 71 | opt = [ 72 | "vips" 73 | ] 74 | 75 | [tool.osreqs.debian] 76 | build = [ 77 | "python3-pip" 78 | ] 79 | run = [ 80 | "python3" 81 | ] 82 | opt = [] 83 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bottle==0.13.* 2 | waitress==3.0.* 3 | doreah==2.0.* 4 | nimrodel==0.8.* 5 | setproctitle==1.3.* 6 | jinja2==3.1.* 7 | lru-dict==1.3.* 8 | psutil==5.9.* 9 | sqlalchemy==2.0 10 | python-datauri==3.0.* 11 | python-magic==0.4.* 12 | requests==2.32.* 13 | toml==0.10.* 14 | PyYAML==6.0.* 15 | 16 | -------------------------------------------------------------------------------- /requirements_extra.txt: -------------------------------------------------------------------------------- 1 | pyvips==2.2.* 2 | 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krateng/maloja/9e44cc3ce6d4259c32026ba50ee934e024b43a7a/screenshot.png --------------------------------------------------------------------------------