mirror of
https://github.com/krateng/maloja.git
synced 2025-04-16 00:40:32 +03:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
f23dde2057
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1 +1 @@
|
||||
custom: ["https://flattr.com/@Krateng", "https://paypal.me/krateng"]
|
||||
custom: ["https://paypal.me/krateng"]
|
||||
|
@ -12,6 +12,7 @@ COPY --chown=abc:abc ./requirements.txt ./requirements.txt
|
||||
# it may be possible to decrease image size slightly by using build stage and
|
||||
# copying all site-packages to runtime stage but the image is already pretty small
|
||||
RUN \
|
||||
echo "" && \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache --virtual=build-deps \
|
||||
gcc \
|
||||
@ -23,18 +24,22 @@ RUN \
|
||||
libc-dev \
|
||||
py3-pip \
|
||||
linux-headers && \
|
||||
echo "" && \
|
||||
echo "**** install runtime packages ****" && \
|
||||
apk add --no-cache \
|
||||
python3 \
|
||||
py3-lxml \
|
||||
tzdata && \
|
||||
echo "" && \
|
||||
echo "**** install pip dependencies ****" && \
|
||||
python3 -m ensurepip && \
|
||||
pip3 install -U --no-cache-dir \
|
||||
pip \
|
||||
wheel && \
|
||||
echo "" && \
|
||||
echo "**** install maloja requirements ****" && \
|
||||
pip3 install --no-cache-dir -r requirements.txt && \
|
||||
echo "" && \
|
||||
echo "**** cleanup ****" && \
|
||||
apk del --purge \
|
||||
build-deps && \
|
||||
@ -47,6 +52,7 @@ RUN \
|
||||
COPY --chown=abc:abc . .
|
||||
|
||||
RUN \
|
||||
echo "" && \
|
||||
echo "**** install maloja ****" && \
|
||||
apk add --no-cache --virtual=install-deps \
|
||||
py3-pip && \
|
||||
|
103
README.md
103
README.md
@ -4,10 +4,7 @@
|
||||
[](https://pypi.org/project/malojaserver/)
|
||||
[](https://hub.docker.com/r/krateng/maloja)
|
||||
|
||||
[](https://github.com/krateng/maloja/blob/master/LICENSE)
|
||||
[](https://codeclimate.com/github/krateng/maloja)
|
||||
|
||||
Simple self-hosted music scrobble database to create personal listening statistics. No recommendations, no social network, no nonsense.
|
||||
Simple self-hosted music scrobble database to create personal listening statistics.
|
||||
|
||||

|
||||
|
||||
@ -18,9 +15,9 @@ You can check [my own Maloja page](https://maloja.krateng.ch) as an example inst
|
||||
* [Features](#features)
|
||||
* [How to install](#how-to-install)
|
||||
* [Requirements](#requirements)
|
||||
* [Docker / Podman](#docker--podman)
|
||||
* [PyPI](#pypi)
|
||||
* [From Source](#from-source)
|
||||
* [Docker / Podman](#docker--podman)
|
||||
* [Extras](#extras)
|
||||
* [How to use](#how-to-use)
|
||||
* [Basic control](#basic-control)
|
||||
@ -47,10 +44,46 @@ You can check [my own Maloja page](https://maloja.krateng.ch) as an example inst
|
||||
|
||||
Maloja should run on any x86 or ARM machine that runs Python.
|
||||
|
||||
I can support you with issues best if you use **Alpine Linux**.
|
||||
It is highly recommended to use **Docker** or **Podman**.
|
||||
|
||||
Your CPU should have a single core passmark score of at the very least 1500. 500 MB RAM should give you a decent experience, but performance will benefit greatly from up to 2 GB.
|
||||
|
||||
### Docker / Podman
|
||||
|
||||
Pull the [latest image](https://hub.docker.com/r/krateng/maloja) or check out the repository and use the included Containerfile.
|
||||
|
||||
Of note are these settings which should be passed as environmental variables to the container:
|
||||
|
||||
* `MALOJA_SKIP_SETUP` -- Make the server setup process non-interactive. Maloja will not work properly in a container without this variable set. This is done by default in the provided Containerfile.
|
||||
* `MALOJA_FORCE_PASSWORD` -- Set an admin password for Maloja. You only need this on the first run.
|
||||
* `MALOJA_DATA_DIRECTORY` -- Set the directory in the container where configuration folders/files should be located
|
||||
* Mount a [volume](https://docs.docker.com/engine/reference/builder/#volume) to the specified directory to access these files outside the container (and to make them persistent)
|
||||
|
||||
You must publish a port on your host machine to bind to the container's web port (default 42010). The container uses IPv4 per default.
|
||||
|
||||
An example of a minimum run configuration to access maloja via `localhost:42010`:
|
||||
|
||||
```console
|
||||
docker run -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||
```
|
||||
|
||||
#### Linux Host
|
||||
|
||||
**NOTE:** If you are using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_) this DOES NOT apply to you.
|
||||
|
||||
If you are running Docker on a **Linux Host** you should specify `user:group` ids of the user who owns the folder on the host machine bound to `MALOJA_DATA_DIRECTORY` in order to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
|
||||
|
||||
To get the UID and GID for the current user run these commands from a terminal:
|
||||
|
||||
* `id -u` -- prints UID (EX `1000`)
|
||||
* `id -g` -- prints GID (EX `1001`)
|
||||
|
||||
The modified run command with these variables would look like:
|
||||
|
||||
```console
|
||||
docker run -e PUID=1000 -e PGID=1001 -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||
```
|
||||
|
||||
### PyPI
|
||||
|
||||
You can install Maloja with
|
||||
@ -78,41 +111,6 @@ Then install all the requirements and build the package, e.g.:
|
||||
pip install .
|
||||
```
|
||||
|
||||
### Docker / Podman
|
||||
|
||||
Pull the [latest image](https://hub.docker.com/r/krateng/maloja) or check out the repository and use the included Containerfile.
|
||||
|
||||
Of note are these settings which should be passed as environmental variables to the container:
|
||||
|
||||
* `MALOJA_SKIP_SETUP` -- Make the server setup process non-interactive. Maloja will not work properly in a container without this variable set. This is done by default in the provided Containerfile.
|
||||
* `MALOJA_FORCE_PASSWORD` -- Set an admin password for Maloja. You only need this on the first run.
|
||||
* `MALOJA_DATA_DIRECTORY` -- Set the directory in the container where configuration folders/files should be located
|
||||
* Mount a [volume](https://docs.docker.com/engine/reference/builder/#volume) to the specified directory to access these files outside the container (and to make them persistent)
|
||||
|
||||
You must publish a port on your host machine to bind to the container's web port (default 42010). The container uses IPv4 per default.
|
||||
|
||||
An example of a minimum run configuration to access maloja via `localhost:42010`:
|
||||
|
||||
```console
|
||||
docker run -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||
```
|
||||
|
||||
#### Linux Host
|
||||
|
||||
**NOTE:** If you are using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_) this DOES NOT apply to you.
|
||||
|
||||
If you are running Docker on a **Linux Host** you should specify `user:group` ids of the user who owns the folder on the host machine bound to `MALOJA_DATA_DIRECTORY` in order to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
|
||||
|
||||
To get the UID and GID for the current user run these commands from a terminal:
|
||||
|
||||
* `id -u` -- prints UID (EX `1000`)
|
||||
* `id -g` -- prints GID (EX `1001`)
|
||||
|
||||
The modified run command with these variables would look like:
|
||||
|
||||
```console
|
||||
docker run -e PUID=1000 -e PGID=1001 -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||
```
|
||||
|
||||
### Extras
|
||||
|
||||
@ -120,31 +118,13 @@ The modified run command with these variables would look like:
|
||||
|
||||
* Put your server behind a reverse proxy for SSL encryption. Make sure that you're proxying to the IPv6 or IPv4 address according to your settings.
|
||||
|
||||
* You can set up a cronjob to start your server on system boot, and potentially restart it on a regular basis:
|
||||
|
||||
```
|
||||
@reboot sleep 15 && maloja start
|
||||
42 0 7 * * maloja restart
|
||||
```
|
||||
|
||||
|
||||
## How to use
|
||||
|
||||
### Basic control
|
||||
|
||||
Start and stop the server in the background with
|
||||
|
||||
```console
|
||||
maloja start
|
||||
maloja stop
|
||||
maloja restart
|
||||
```
|
||||
|
||||
If you need to run the server in the foreground, use
|
||||
|
||||
```console
|
||||
maloja run
|
||||
```
|
||||
When not running in a container, you can run the application with `maloja run`. You can also run it in the background with
|
||||
`maloja start` and `maloja stop`, but this might not be supported in the future.
|
||||
|
||||
|
||||
### Data
|
||||
@ -164,7 +144,6 @@ If you would like to import your previous scrobbles, use the command `maloja imp
|
||||
maloja import my_last_fm_export.csv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To backup your data, run `maloja backup`, optional with `--include_images`.
|
||||
|
||||
|
@ -41,6 +41,3 @@ minor_release_name: "Soyeon"
|
||||
- "[Bugfix] Fixed image display on Safari"
|
||||
- "[Bugfix] Fixed entity editing on Firefox"
|
||||
- "[Bugfix] Made compatibile with SQLAlchemy 2.0"
|
||||
upcoming:
|
||||
notes:
|
||||
- "[Bugfix] Fixed configuration of time format"
|
||||
|
19
dev/releases/3.2.yml
Normal file
19
dev/releases/3.2.yml
Normal file
@ -0,0 +1,19 @@
|
||||
minor_release_name: "Nicole"
|
||||
3.2.0:
|
||||
notes:
|
||||
- "[Architecture] Switched to linuxserver.io container base image"
|
||||
- "[Architecture] Reworked image handling"
|
||||
- "[Architecture] Removed pre-calculated stats"
|
||||
- "[Feature] Added support for albums"
|
||||
- "[Feature] New start page"
|
||||
- "[Feature] Added UI for track-artist, track-album and album-artist association"
|
||||
- "[Feature] Added inline UI for association and merging in chart lists"
|
||||
- "[Feature] Added UI selector for including associated artists"
|
||||
- "[Feature] Added UI distinction for associated scrobbles in chart bars"
|
||||
- "[Performance] Improved image rendering"
|
||||
- "[Performance] Optimized several database calls"
|
||||
- "[Bugfix] Fixed configuration of time format"
|
||||
- "[Bugfix] Fixed search on manual scrobble page"
|
||||
- "[Bugfix] Disabled DB maintenance while not running main server"
|
||||
- "[Bugfix] Removed some nonsensical ephemereal database entry creations"
|
||||
- "[Bugfix] Fixed API endpoint for track charts with no artist provided"
|
@ -189,7 +189,7 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.title1}}\"\n}"
|
||||
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.title1}}\",\n \"album\": \"{{data.album}}\",\n \"albumartists\":[\n \"{{data.artist1}}\",\n \"{{data.artist3}}\"\n ]\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
||||
@ -219,7 +219,7 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artists\": [\"{{data.artist1}}\",\"{{data.artist2}}\"],\n \"title\": \"{{data.title1}}\"\n}"
|
||||
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artists\": [\"{{data.artist1}}\",\"{{data.artist2}}\"],\n \"title\": \"{{data.title1}}\",\n \"album\": \"{{data.album}}\",\n \"albumartists\":[\n \"{{data.artist1}}\",\n \"{{data.artist3}}\"\n ]\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
||||
@ -867,6 +867,11 @@
|
||||
"key": "data.title3",
|
||||
"value": "One in a Million"
|
||||
},
|
||||
{
|
||||
"key": "data.album",
|
||||
"value": "The Epic Collection",
|
||||
"type": "default"
|
||||
},
|
||||
{
|
||||
"key": "data.timestamp1",
|
||||
"value": ""
|
||||
|
15
install/install_dependencies_arch.sh
Executable file
15
install/install_dependencies_arch.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env sh
|
||||
pacman -Syu
|
||||
pacman -S --needed \
|
||||
gcc \
|
||||
python3 \
|
||||
libxml2 \
|
||||
libxslt \
|
||||
libffi \
|
||||
glibc \
|
||||
python-pip \
|
||||
linux-headers \
|
||||
python \
|
||||
python-lxml \
|
||||
tzdata \
|
||||
libvips
|
@ -30,13 +30,13 @@ def print_header_info():
|
||||
|
||||
def get_instance():
|
||||
try:
|
||||
return int(subprocess.check_output(["pidof","maloja"]))
|
||||
return int(subprocess.check_output(["pgrep","-x","maloja"]))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_instance_supervisor():
|
||||
try:
|
||||
return int(subprocess.check_output(["pidof","maloja_supervisor"]))
|
||||
return int(subprocess.check_output(["pgrep","-x","maloja_supervisor"]))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@ -130,7 +130,7 @@ def run_supervisor():
|
||||
def debug():
|
||||
os.environ["MALOJA_DEV_MODE"] = 'true'
|
||||
conf.malojaconfig.load_environment()
|
||||
direct()
|
||||
run_server()
|
||||
|
||||
def print_info():
|
||||
print_header_info()
|
||||
@ -141,14 +141,15 @@ def print_info():
|
||||
print(col['lightblue']("Timezone: "),f"UTC{conf.malojaconfig['timezone']:+d}")
|
||||
print()
|
||||
try:
|
||||
import pkg_resources
|
||||
from importlib.metadata import distribution
|
||||
for pkg in ("sqlalchemy","waitress","bottle","doreah","jinja2"):
|
||||
print(col['cyan'] (f"{pkg}:".ljust(13)),pkg_resources.get_distribution(pkg).version)
|
||||
print(col['cyan'] (f"{pkg}:".ljust(13)),distribution(pkg).version)
|
||||
except ImportError:
|
||||
raise
|
||||
print("Could not determine dependency versions.")
|
||||
print()
|
||||
|
||||
@mainfunction({"l":"level","v":"version","V":"version"},flags=['version','include_images'],shield=True)
|
||||
@mainfunction({"l":"level","v":"version","V":"version"},flags=['version','include_images','prefer_existing'],shield=True)
|
||||
def main(*args,**kwargs):
|
||||
|
||||
actions = {
|
||||
@ -166,12 +167,13 @@ def main(*args,**kwargs):
|
||||
"generate":generate.generate_scrobbles, # maloja generate 400
|
||||
"export":tasks.export, # maloja export
|
||||
"apidebug":apidebug.run, # maloja apidebug
|
||||
"parsealbums":tasks.parse_albums, # maloja parsealbums --strategy majority
|
||||
# aux
|
||||
"info":print_info
|
||||
}
|
||||
|
||||
if "version" in kwargs:
|
||||
print(info.VERSION)
|
||||
print(pkginfo.VERSION)
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
|
@ -75,7 +75,7 @@ class Listenbrainz(APIHandler):
|
||||
self.scrobble({
|
||||
'track_artists':[artiststr],
|
||||
'track_title':titlestr,
|
||||
'album_name':albumstr,
|
||||
'album_title':albumstr,
|
||||
'scrobble_time':timestamp,
|
||||
'track_length': additional.get("duration"),
|
||||
**extrafields
|
||||
|
@ -2,10 +2,12 @@ import os
|
||||
import math
|
||||
import traceback
|
||||
|
||||
from bottle import response, static_file, request, FormsDict
|
||||
from bottle import response, static_file, FormsDict
|
||||
|
||||
from inspect import signature
|
||||
|
||||
from doreah.logging import log
|
||||
from doreah.auth import authenticated_api, authenticated_api_with_alternate, authenticated_function
|
||||
from doreah.auth import authenticated_function
|
||||
|
||||
# nimrodel API
|
||||
from nimrodel import EAPI as API
|
||||
@ -18,7 +20,7 @@ from ..pkg_global.conf import malojaconfig, data_dir
|
||||
|
||||
|
||||
from ..__pkginfo__ import VERSION
|
||||
from ..malojauri import uri_to_internal, compose_querystring, internal_to_uri
|
||||
from ..malojauri import uri_to_internal, compose_querystring, internal_to_uri, create_uri
|
||||
from .. import images
|
||||
from ._apikeys import apikeystore, api_key_correct
|
||||
|
||||
@ -72,6 +74,14 @@ errors = {
|
||||
'desc':"The database is being upgraded. Please try again later."
|
||||
}
|
||||
}),
|
||||
database.exceptions.EntityDoesNotExist: lambda e: (404,{
|
||||
"status":"error",
|
||||
"error":{
|
||||
'type':'entity_does_not_exist',
|
||||
'value':e.entitydict,
|
||||
'desc':"This entity does not exist in the database."
|
||||
}
|
||||
}),
|
||||
images.MalformedB64: lambda e: (400,{
|
||||
"status":"failure",
|
||||
"error":{
|
||||
@ -91,6 +101,8 @@ errors = {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
# decorator to catch exceptions and return proper json responses
|
||||
def catch_exceptions(func):
|
||||
def protector(*args,**kwargs):
|
||||
try:
|
||||
@ -105,9 +117,11 @@ def catch_exceptions(func):
|
||||
|
||||
protector.__doc__ = func.__doc__
|
||||
protector.__annotations__ = func.__annotations__
|
||||
protector.__name__ = f"EXCPR_{func.__name__}"
|
||||
return protector
|
||||
|
||||
|
||||
# decorator to expand the docstring with common arguments for the API explorer. DOESNT WRAP
|
||||
def add_common_args_to_docstring(filterkeys=False,limitkeys=False,delimitkeys=False,amountkeys=False):
|
||||
def decorator(func):
|
||||
timeformats = "Possible formats include '2022', '2022/08', '2022/08/01', '2022/W42', 'today', 'thismonth', 'monday', 'august'"
|
||||
@ -141,6 +155,64 @@ def add_common_args_to_docstring(filterkeys=False,limitkeys=False,delimitkeys=Fa
|
||||
return decorator
|
||||
|
||||
|
||||
# decorator to take the URI keys and convert them into internal keys
|
||||
def convert_kwargs(func):
|
||||
|
||||
#params = tuple(p for p in signature(func).parameters)
|
||||
|
||||
def wrapper(*args,albumartist:Multi[str]=[],trackartist:Multi[str]=[],**kwargs):
|
||||
|
||||
kwargs = FormsDict(kwargs)
|
||||
for a in albumartist:
|
||||
kwargs.append("albumartist",a)
|
||||
for a in trackartist:
|
||||
kwargs.append("trackartist",a)
|
||||
|
||||
k_filter, k_limit, k_delimit, k_amount, k_special = uri_to_internal(kwargs,api=True)
|
||||
|
||||
try:
|
||||
return func(*args,k_filter=k_filter, k_limit=k_limit, k_delimit=k_delimit, k_amount=k_amount)
|
||||
except TypeError:
|
||||
return func(*args,k_filter=k_filter, k_limit=k_limit, k_delimit=k_delimit, k_amount=k_amount,k_special=k_special)
|
||||
# TODO: ....really?
|
||||
|
||||
wrapper.__doc__ = func.__doc__
|
||||
wrapper.__name__ = f"CVKWA_{func.__name__}"
|
||||
return wrapper
|
||||
|
||||
|
||||
# decorator to add pagination info to endpoints (like links to other pages)
|
||||
# this expects already converted uri args!!!
|
||||
def add_pagination(endpoint,filterkeys=False,limitkeys=False,delimitkeys=False):
|
||||
|
||||
def decorator(func):
|
||||
def wrapper(*args,k_filter, k_limit, k_delimit, k_amount):
|
||||
|
||||
keydicts = []
|
||||
if filterkeys: keydicts.append(k_filter)
|
||||
if limitkeys: keydicts.append(k_limit)
|
||||
if delimitkeys: keydicts.append(k_delimit)
|
||||
keydicts.append(k_amount)
|
||||
|
||||
|
||||
result = func(*args,k_filter=k_filter, k_limit=k_limit, k_delimit=k_delimit, k_amount=k_amount)
|
||||
|
||||
result['pagination'] = {
|
||||
'page': k_amount['page'],
|
||||
'perpage': k_amount['perpage'] if (k_amount['perpage'] is not math.inf) else None,
|
||||
'next_page': create_uri(api.pathprefix + '/' + endpoint,*keydicts,{'page':k_amount['page']+1}) if len(result.get('list',[]))==k_amount['perpage'] else None,
|
||||
'prev_page': create_uri(api.pathprefix + '/' + endpoint,*keydicts,{'page':k_amount['page']-1}) if k_amount['page'] > 0 else None
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
wrapper.__doc__ = func.__doc__
|
||||
wrapper.__annotations__ = func.__annotations__
|
||||
wrapper.__name__ = f"PGNAT_{func.__name__}"
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@api.get("test")
|
||||
@catch_exceptions
|
||||
@ -194,20 +266,22 @@ def server_info():
|
||||
@api.get("scrobbles")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True,amountkeys=True)
|
||||
def get_scrobbles_external(**keys):
|
||||
@convert_kwargs
|
||||
@add_pagination("scrobbles",filterkeys=True,limitkeys=True)
|
||||
def get_scrobbles_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns a list of scrobbles.
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary
|
||||
"""
|
||||
k_filter, k_time, _, k_amount, _ = uri_to_internal(keys,api=True)
|
||||
ckeys = {**k_filter, **k_time, **k_amount}
|
||||
|
||||
ckeys = {**k_filter, **k_limit, **k_amount}
|
||||
result = database.get_scrobbles(**ckeys)
|
||||
|
||||
offset = (k_amount.get('page') * k_amount.get('perpage')) if k_amount.get('perpage') is not math.inf else 0
|
||||
result = result[offset:]
|
||||
if k_amount.get('perpage') is not math.inf: result = result[:k_amount.get('perpage')]
|
||||
# this should now all be served by the inner function
|
||||
#offset = (k_amount.get('page') * k_amount.get('perpage')) if k_amount.get('perpage') is not math.inf else 0
|
||||
#result = result[offset:]
|
||||
#if k_amount.get('perpage') is not math.inf: result = result[:k_amount.get('perpage')]
|
||||
|
||||
return {
|
||||
"status":"ok",
|
||||
@ -218,15 +292,15 @@ def get_scrobbles_external(**keys):
|
||||
@api.get("numscrobbles")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True,amountkeys=True)
|
||||
def get_scrobbles_num_external(**keys):
|
||||
@convert_kwargs
|
||||
def get_scrobbles_num_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns amount of scrobbles.
|
||||
|
||||
:return: amount (Integer)
|
||||
:rtype: Dictionary
|
||||
"""
|
||||
k_filter, k_time, _, k_amount, _ = uri_to_internal(keys)
|
||||
ckeys = {**k_filter, **k_time, **k_amount}
|
||||
|
||||
ckeys = {**k_filter, **k_limit, **k_amount}
|
||||
result = database.get_scrobbles_num(**ckeys)
|
||||
|
||||
return {
|
||||
@ -235,19 +309,18 @@ def get_scrobbles_num_external(**keys):
|
||||
}
|
||||
|
||||
|
||||
|
||||
@api.get("tracks")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True)
|
||||
def get_tracks_external(**keys):
|
||||
"""Returns all tracks (optionally of an artist).
|
||||
@convert_kwargs
|
||||
def get_tracks_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns all tracks (optionally of an artist or on an album).
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary
|
||||
"""
|
||||
k_filter, _, _, _, _ = uri_to_internal(keys,forceArtist=True)
|
||||
ckeys = {**k_filter}
|
||||
|
||||
ckeys = {**k_filter}
|
||||
result = database.get_tracks(**ckeys)
|
||||
|
||||
return {
|
||||
@ -256,15 +329,16 @@ def get_tracks_external(**keys):
|
||||
}
|
||||
|
||||
|
||||
|
||||
@api.get("artists")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring()
|
||||
def get_artists_external():
|
||||
@convert_kwargs
|
||||
def get_artists_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns all artists.
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
|
||||
result = database.get_artists()
|
||||
|
||||
return {
|
||||
@ -273,20 +347,36 @@ def get_artists_external():
|
||||
}
|
||||
|
||||
|
||||
@api.get("albums")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True)
|
||||
@convert_kwargs
|
||||
def get_albums_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns all albums (optionally of an artist).
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
|
||||
ckeys = {**k_filter}
|
||||
result = database.get_albums(**ckeys)
|
||||
|
||||
return {
|
||||
"status":"ok",
|
||||
"list":result
|
||||
}
|
||||
|
||||
|
||||
@api.get("charts/artists")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(limitkeys=True)
|
||||
def get_charts_artists_external(**keys):
|
||||
@convert_kwargs
|
||||
def get_charts_artists_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns artist charts
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
_, k_time, _, _, _ = uri_to_internal(keys)
|
||||
ckeys = {**k_time}
|
||||
|
||||
ckeys = {**k_limit}
|
||||
result = database.get_charts_artists(**ckeys)
|
||||
|
||||
return {
|
||||
@ -295,18 +385,17 @@ def get_charts_artists_external(**keys):
|
||||
}
|
||||
|
||||
|
||||
|
||||
@api.get("charts/tracks")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True)
|
||||
def get_charts_tracks_external(**keys):
|
||||
@convert_kwargs
|
||||
def get_charts_tracks_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns track charts
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
k_filter, k_time, _, _, _ = uri_to_internal(keys,forceArtist=True)
|
||||
ckeys = {**k_filter, **k_time}
|
||||
|
||||
ckeys = {**k_filter, **k_limit}
|
||||
result = database.get_charts_tracks(**ckeys)
|
||||
|
||||
return {
|
||||
@ -315,19 +404,36 @@ def get_charts_tracks_external(**keys):
|
||||
}
|
||||
|
||||
|
||||
@api.get("charts/albums")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True)
|
||||
@convert_kwargs
|
||||
def get_charts_albums_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns album charts
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
|
||||
ckeys = {**k_filter, **k_limit}
|
||||
result = database.get_charts_albums(**ckeys)
|
||||
|
||||
return {
|
||||
"status":"ok",
|
||||
"list":result
|
||||
}
|
||||
|
||||
|
||||
@api.get("pulse")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True,delimitkeys=True,amountkeys=True)
|
||||
def get_pulse_external(**keys):
|
||||
@convert_kwargs
|
||||
def get_pulse_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns amounts of scrobbles in specified time frames
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
k_filter, k_time, k_internal, k_amount, _ = uri_to_internal(keys)
|
||||
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
|
||||
|
||||
ckeys = {**k_filter, **k_limit, **k_delimit, **k_amount}
|
||||
results = database.get_pulse(**ckeys)
|
||||
|
||||
return {
|
||||
@ -336,19 +442,17 @@ def get_pulse_external(**keys):
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@api.get("performance")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True,delimitkeys=True,amountkeys=True)
|
||||
def get_performance_external(**keys):
|
||||
@convert_kwargs
|
||||
def get_performance_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns artist's or track's rank in specified time frames
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
k_filter, k_time, k_internal, k_amount, _ = uri_to_internal(keys)
|
||||
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
|
||||
|
||||
ckeys = {**k_filter, **k_limit, **k_delimit, **k_amount}
|
||||
results = database.get_performance(**ckeys)
|
||||
|
||||
return {
|
||||
@ -362,14 +466,14 @@ def get_performance_external(**keys):
|
||||
@api.get("top/artists")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(limitkeys=True,delimitkeys=True)
|
||||
def get_top_artists_external(**keys):
|
||||
@convert_kwargs
|
||||
def get_top_artists_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns respective number 1 artists in specified time frames
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
_, k_time, k_internal, _, _ = uri_to_internal(keys)
|
||||
ckeys = {**k_time, **k_internal}
|
||||
|
||||
ckeys = {**k_limit, **k_delimit}
|
||||
results = database.get_top_artists(**ckeys)
|
||||
|
||||
return {
|
||||
@ -378,22 +482,19 @@ def get_top_artists_external(**keys):
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@api.get("top/tracks")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(limitkeys=True,delimitkeys=True)
|
||||
def get_top_tracks_external(**keys):
|
||||
@convert_kwargs
|
||||
def get_top_tracks_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns respective number 1 tracks in specified time frames
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
_, k_time, k_internal, _, _ = uri_to_internal(keys)
|
||||
ckeys = {**k_time, **k_internal}
|
||||
|
||||
# IMPLEMENT THIS FOR TOP TRACKS OF ARTIST AS WELL?
|
||||
|
||||
ckeys = {**k_limit, **k_delimit}
|
||||
results = database.get_top_tracks(**ckeys)
|
||||
# IMPLEMENT THIS FOR TOP TRACKS OF ARTIST/ALBUM AS WELL?
|
||||
|
||||
return {
|
||||
"status":"ok",
|
||||
@ -401,17 +502,36 @@ def get_top_tracks_external(**keys):
|
||||
}
|
||||
|
||||
|
||||
@api.get("top/albums")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(limitkeys=True,delimitkeys=True)
|
||||
@convert_kwargs
|
||||
def get_top_albums_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns respective number 1 albums in specified time frames
|
||||
|
||||
:return: list (List)
|
||||
:rtype: Dictionary"""
|
||||
|
||||
ckeys = {**k_limit, **k_delimit}
|
||||
results = database.get_top_albums(**ckeys)
|
||||
# IMPLEMENT THIS FOR TOP ALBUMS OF ARTIST AS WELL?
|
||||
|
||||
return {
|
||||
"status":"ok",
|
||||
"list":results
|
||||
}
|
||||
|
||||
|
||||
@api.get("artistinfo")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True)
|
||||
def artist_info_external(**keys):
|
||||
@convert_kwargs
|
||||
def artist_info_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns information about an artist
|
||||
|
||||
:return: artist (String), scrobbles (Integer), position (Integer), associated (List), medals (Mapping), topweeks (Integer)
|
||||
:rtype: Dictionary"""
|
||||
k_filter, _, _, _, _ = uri_to_internal(keys,forceArtist=True)
|
||||
|
||||
ckeys = {**k_filter}
|
||||
|
||||
return database.artist_info(**ckeys)
|
||||
@ -421,21 +541,31 @@ def artist_info_external(**keys):
|
||||
@api.get("trackinfo")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True)
|
||||
def track_info_external(artist:Multi[str]=[],**keys):
|
||||
@convert_kwargs
|
||||
def track_info_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns information about a track
|
||||
|
||||
:return: track (Mapping), scrobbles (Integer), position (Integer), medals (Mapping), certification (String), topweeks (Integer)
|
||||
:rtype: Dictionary"""
|
||||
# transform into a multidict so we can use our nomral uri_to_internal function
|
||||
keys = FormsDict(keys)
|
||||
for a in artist:
|
||||
keys.append("artist",a)
|
||||
k_filter, _, _, _, _ = uri_to_internal(keys,forceTrack=True)
|
||||
ckeys = {**k_filter}
|
||||
|
||||
ckeys = {**k_filter}
|
||||
return database.track_info(**ckeys)
|
||||
|
||||
|
||||
@api.get("albuminfo")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True)
|
||||
@convert_kwargs
|
||||
def album_info_external(k_filter, k_limit, k_delimit, k_amount):
|
||||
"""Returns information about an album
|
||||
|
||||
:return: album (Mapping), scrobbles (Integer), position (Integer), medals (Mapping), certification (String), topweeks (Integer)
|
||||
:rtype: Dictionary"""
|
||||
|
||||
ckeys = {**k_filter}
|
||||
return database.album_info(**ckeys)
|
||||
|
||||
|
||||
@api.post("newscrobble")
|
||||
@authenticated_function(alternate=api_key_correct,api=True,pass_auth_result_as='auth_result')
|
||||
@catch_exceptions
|
||||
@ -444,7 +574,7 @@ def post_scrobble(
|
||||
artists:list=[],
|
||||
title:str="",
|
||||
album:str=None,
|
||||
albumartists:list=[],
|
||||
albumartists:list=None,
|
||||
duration:int=None,
|
||||
length:int=None,
|
||||
time:int=None,
|
||||
@ -470,7 +600,7 @@ def post_scrobble(
|
||||
rawscrobble = {
|
||||
'track_artists':(artist or []) + artists,
|
||||
'track_title':title,
|
||||
'album_name':album,
|
||||
'album_title':album,
|
||||
'album_artists':albumartists,
|
||||
'scrobble_duration':duration,
|
||||
'track_length':length,
|
||||
@ -478,7 +608,7 @@ def post_scrobble(
|
||||
}
|
||||
|
||||
# for logging purposes, don't pass values that we didn't actually supply
|
||||
rawscrobble = {k:rawscrobble[k] for k in rawscrobble if rawscrobble[k]}
|
||||
rawscrobble = {k:rawscrobble[k] for k in rawscrobble if rawscrobble[k] is not None} # [] should be passed
|
||||
|
||||
|
||||
result = database.incoming_scrobble(
|
||||
@ -494,19 +624,23 @@ def post_scrobble(
|
||||
'artists':result['track']['artists'],
|
||||
'title':result['track']['title']
|
||||
},
|
||||
'desc':f"Scrobbled {result['track']['title']} by {', '.join(result['track']['artists'])}"
|
||||
'desc':f"Scrobbled {result['track']['title']} by {', '.join(result['track']['artists'])}",
|
||||
'warnings':[]
|
||||
}
|
||||
if extra_kwargs:
|
||||
responsedict['warnings'] = [
|
||||
responsedict['warnings'] += [
|
||||
{'type':'invalid_keyword_ignored','value':k,
|
||||
'desc':"This key was not recognized by the server and has been discarded."}
|
||||
for k in extra_kwargs
|
||||
]
|
||||
if artist and artists:
|
||||
responsedict['warnings'] = [
|
||||
responsedict['warnings'] += [
|
||||
{'type':'mixed_schema','value':['artist','artists'],
|
||||
'desc':"These two fields are meant as alternative methods to submit information. Use of both is discouraged, but works at the moment."}
|
||||
]
|
||||
|
||||
if len(responsedict['warnings']) == 0: del responsedict['warnings']
|
||||
|
||||
return responsedict
|
||||
|
||||
|
||||
@ -515,21 +649,18 @@ def post_scrobble(
|
||||
@api.post("addpicture")
|
||||
@authenticated_function(alternate=api_key_correct,api=True)
|
||||
@catch_exceptions
|
||||
def add_picture(b64,artist:Multi=[],title=None):
|
||||
"""Uploads a new image for an artist or track.
|
||||
@convert_kwargs
|
||||
def add_picture(k_filter, k_limit, k_delimit, k_amount, k_special):
|
||||
"""Uploads a new image for an artist, album or track.
|
||||
|
||||
param string b64: Base 64 representation of the image
|
||||
param string artist: Artist name. Can be supplied multiple times for tracks with multiple artists.
|
||||
param string title: Title of the track. Optional.
|
||||
|
||||
"""
|
||||
keys = FormsDict()
|
||||
for a in artist:
|
||||
keys.append("artist",a)
|
||||
if title is not None: keys.append("title",title)
|
||||
k_filter, _, _, _, _ = uri_to_internal(keys)
|
||||
|
||||
if "associated" in k_filter: del k_filter["associated"]
|
||||
if "track" in k_filter: k_filter = k_filter["track"]
|
||||
url = images.set_image(b64,**k_filter)
|
||||
elif "album" in k_filter: k_filter = k_filter["album"]
|
||||
url = images.set_image(k_special['b64'],**k_filter)
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
@ -586,6 +717,7 @@ def search(**keys):
|
||||
|
||||
artists = database.db_search(query,type="ARTIST")
|
||||
tracks = database.db_search(query,type="TRACK")
|
||||
albums = database.db_search(query,type="ALBUM")
|
||||
|
||||
|
||||
|
||||
@ -593,6 +725,7 @@ def search(**keys):
|
||||
# also, shorter is better (because longer titles would be easier to further specify)
|
||||
artists.sort(key=lambda x: ((0 if x.lower().startswith(query) else 1 if " " + query in x.lower() else 2),len(x)))
|
||||
tracks.sort(key=lambda x: ((0 if x["title"].lower().startswith(query) else 1 if " " + query in x["title"].lower() else 2),len(x["title"])))
|
||||
albums.sort(key=lambda x: ((0 if x["albumtitle"].lower().startswith(query) else 1 if " " + query in x["albumtitle"].lower() else 2),len(x["albumtitle"])))
|
||||
|
||||
# add links
|
||||
artists_result = []
|
||||
@ -613,7 +746,22 @@ def search(**keys):
|
||||
}
|
||||
tracks_result.append(result)
|
||||
|
||||
return {"artists":artists_result[:max_],"tracks":tracks_result[:max_]}
|
||||
albums_result = []
|
||||
for al in albums:
|
||||
result = {
|
||||
'album': al,
|
||||
'link': "/album?" + compose_querystring(internal_to_uri({"album":al})),
|
||||
'image': images.get_album_image(al)
|
||||
}
|
||||
mutable_result = result.copy()
|
||||
mutable_result['album'] = result['album'].copy()
|
||||
if not mutable_result['album']['artists']: mutable_result['album']['displayArtist'] = malojaconfig["DEFAULT_ALBUM_ARTIST"]
|
||||
# we don't wanna actually mutate the dict here because this is in the cache
|
||||
# TODO: This should be globally solved!!!!! immutable dicts with mutable overlays???
|
||||
# this is a major flaw in the architecture!
|
||||
albums_result.append(mutable_result)
|
||||
|
||||
return {"artists":artists_result[:max_],"tracks":tracks_result[:max_],"albums":albums_result[:max_]}
|
||||
|
||||
|
||||
@api.post("newrule")
|
||||
@ -708,6 +856,16 @@ def edit_track(id,title):
|
||||
"status":"success"
|
||||
}
|
||||
|
||||
@api.post("edit_album")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def edit_album(id,albumtitle):
|
||||
"""Internal Use Only"""
|
||||
result = database.edit_album(id,{'albumtitle':albumtitle})
|
||||
return {
|
||||
"status":"success"
|
||||
}
|
||||
|
||||
|
||||
@api.post("merge_tracks")
|
||||
@authenticated_function(api=True)
|
||||
@ -716,7 +874,8 @@ def merge_tracks(target_id,source_ids):
|
||||
"""Internal Use Only"""
|
||||
result = database.merge_tracks(target_id,source_ids)
|
||||
return {
|
||||
"status":"success"
|
||||
"status":"success",
|
||||
"desc":f"{', '.join(src['title'] for src in result['sources'])} were merged into {result['target']['title']}"
|
||||
}
|
||||
|
||||
@api.post("merge_artists")
|
||||
@ -726,9 +885,57 @@ def merge_artists(target_id,source_ids):
|
||||
"""Internal Use Only"""
|
||||
result = database.merge_artists(target_id,source_ids)
|
||||
return {
|
||||
"status":"success"
|
||||
"status":"success",
|
||||
"desc":f"{', '.join(src for src in result['sources'])} were merged into {result['target']}"
|
||||
}
|
||||
|
||||
@api.post("merge_albums")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def merge_artists(target_id,source_ids):
|
||||
"""Internal Use Only"""
|
||||
result = database.merge_albums(target_id,source_ids)
|
||||
return {
|
||||
"status":"success",
|
||||
"desc":f"{', '.join(src['albumtitle'] for src in result['sources'])} were merged into {result['target']['albumtitle']}"
|
||||
}
|
||||
|
||||
@api.post("associate_albums_to_artist")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def associate_albums_to_artist(target_id,source_ids,remove=False):
|
||||
result = database.associate_albums_to_artist(target_id,source_ids,remove=remove)
|
||||
descword = "removed" if remove else "added"
|
||||
if result:
|
||||
return {
|
||||
"status":"success",
|
||||
"desc":f"{result['target']} was {descword} as album artist of {', '.join(src['albumtitle'] for src in result['sources'])}"
|
||||
}
|
||||
|
||||
@api.post("associate_tracks_to_artist")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def associate_tracks_to_artist(target_id,source_ids,remove=False):
|
||||
result = database.associate_tracks_to_artist(target_id,source_ids,remove=remove)
|
||||
descword = "removed" if remove else "added"
|
||||
if result:
|
||||
return {
|
||||
"status":"success",
|
||||
"desc":f"{result['target']} was {descword} as artist for {', '.join(src['title'] for src in result['sources'])}"
|
||||
}
|
||||
|
||||
@api.post("associate_tracks_to_album")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def associate_tracks_to_album(target_id,source_ids):
|
||||
result = database.associate_tracks_to_album(target_id,source_ids)
|
||||
if result:
|
||||
return {
|
||||
"status":"success",
|
||||
"desc":f"{', '.join(src['title'] for src in result['sources'])} were " + f"added to {result['target']['albumtitle']}" if target_id else "removed from their album"
|
||||
}
|
||||
|
||||
|
||||
@api.post("reparse_scrobble")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
|
@ -26,6 +26,7 @@ class CleanerAgent:
|
||||
self.rules_belongtogether = [r[1] for r in rawrules if r[0]=="belongtogether"]
|
||||
self.rules_notanartist = [r[1] for r in rawrules if r[0]=="notanartist"]
|
||||
self.rules_replacetitle = {r[1].lower():r[2] for r in rawrules if r[0]=="replacetitle"}
|
||||
self.rules_replacealbumtitle = {r[1].lower():r[2] for r in rawrules if r[0]=="replacealbumtitle"}
|
||||
self.rules_replaceartist = {r[1].lower():r[2] for r in rawrules if r[0]=="replaceartist"}
|
||||
self.rules_ignoreartist = [r[1].lower() for r in rawrules if r[0]=="ignoreartist"]
|
||||
self.rules_addartists = {r[2].lower():(r[1].lower(),r[3]) for r in rawrules if r[0]=="addartists"}
|
||||
@ -188,9 +189,14 @@ class CleanerAgent:
|
||||
if st in title.lower(): artists += self.rules_artistintitle[st].split("␟")
|
||||
return (title,artists)
|
||||
|
||||
def parseAlbumtitle(self,t):
|
||||
if t.strip().lower() in self.rules_replacealbumtitle:
|
||||
return self.rules_replacealbumtitle[t.strip().lower()]
|
||||
|
||||
t = t.replace("[","(").replace("]",")")
|
||||
|
||||
|
||||
t = t.strip()
|
||||
return t
|
||||
|
||||
|
||||
def flatten(lis):
|
||||
|
0
maloja/data_files/cache/images/dummy
vendored
Normal file
0
maloja/data_files/cache/images/dummy
vendored
Normal file
@ -98,6 +98,7 @@ countas Jeongyeon TWICE
|
||||
countas Chaeyoung TWICE
|
||||
countas Nayeon TWICE
|
||||
countas Sana TWICE
|
||||
countas MISAMO TWICE
|
||||
|
||||
# AOA
|
||||
countas AOA Black AOA
|
||||
@ -182,6 +183,9 @@ countas Akali K/DA
|
||||
# (G)I-DLE
|
||||
countas Soyeon (G)I-DLE
|
||||
countas Miyeon (G)I-DLE
|
||||
countas Yuqi (G)I-DLE
|
||||
countas Minnie (G)I-DLE
|
||||
countas Shuhua (G)I-DLE
|
||||
replaceartist Jeon Soyeon Soyeon
|
||||
|
||||
|
||||
@ -208,6 +212,12 @@ countas Wonyoung IVE
|
||||
countas Yujin IVE
|
||||
countas Gaeul IVE
|
||||
|
||||
# Pristin
|
||||
countas Pristin V Pristin
|
||||
|
||||
# CLC
|
||||
countas Sorn CLC
|
||||
|
||||
# Popular Remixes
|
||||
artistintitle Areia Remix Areia
|
||||
artistintitle Areia Kpop Areia
|
||||
|
Can't render this file because it has a wrong number of fields in line 5.
|
0
maloja/data_files/state/images/albums/dummy
Normal file
0
maloja/data_files/state/images/albums/dummy
Normal file
@ -1,17 +1,27 @@
|
||||
# server
|
||||
from bottle import request, response, FormsDict
|
||||
|
||||
|
||||
# decorator that makes sure this function is only run in normal operation,
|
||||
# not when we run a task that needs to access the database
|
||||
def no_aux_mode(func):
|
||||
def wrapper(*args,**kwargs):
|
||||
from ..pkg_global import conf
|
||||
if conf.AUX_MODE: return
|
||||
return func(*args,**kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# rest of the project
|
||||
from ..cleanup import CleanerAgent
|
||||
from .. import images
|
||||
from ..malojatime import register_scrobbletime, time_stamps, ranges, alltime
|
||||
from ..malojatime import register_scrobbletime, ranges, alltime, today, thisweek, thisyear, MTRangeComposite
|
||||
from ..malojauri import uri_to_internal, internal_to_uri, compose_querystring
|
||||
from ..thirdparty import proxy_scrobble_all
|
||||
from ..pkg_global.conf import data_dir, malojaconfig
|
||||
from ..apis import apikeystore
|
||||
#db
|
||||
from . import sqldb
|
||||
from . import cached
|
||||
from . import dbcache
|
||||
from . import exceptions
|
||||
|
||||
@ -46,10 +56,15 @@ dbstatus = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def waitfordb(func):
|
||||
def newfunc(*args,**kwargs):
|
||||
if not dbstatus['healthy']: raise exceptions.DatabaseNotBuilt()
|
||||
return func(*args,**kwargs)
|
||||
|
||||
newfunc.__name__ = func.__name__
|
||||
return newfunc
|
||||
|
||||
|
||||
@ -93,12 +108,15 @@ def incoming_scrobble(rawscrobble,fix=True,client=None,api=None,dbconn=None):
|
||||
log(f"Incoming scrobble [Client: {client} | API: {api}]: {rawscrobble}")
|
||||
|
||||
scrobbledict = rawscrobble_to_scrobbledict(rawscrobble, fix, client)
|
||||
albumupdate = (malojaconfig["ALBUM_INFORMATION_TRUST"] == 'last')
|
||||
|
||||
sqldb.add_scrobble(scrobbledict,dbconn=dbconn)
|
||||
|
||||
sqldb.add_scrobble(scrobbledict,update_album=albumupdate,dbconn=dbconn)
|
||||
proxy_scrobble_all(scrobbledict['track']['artists'],scrobbledict['track']['title'],scrobbledict['time'])
|
||||
|
||||
dbcache.invalidate_caches(scrobbledict['time'])
|
||||
|
||||
|
||||
#return {"status":"success","scrobble":scrobbledict}
|
||||
return scrobbledict
|
||||
|
||||
@ -130,8 +148,23 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None):
|
||||
scrobbleinfo = {**rawscrobble}
|
||||
if fix:
|
||||
scrobbleinfo['track_artists'],scrobbleinfo['track_title'] = cla.fullclean(scrobbleinfo['track_artists'],scrobbleinfo['track_title'])
|
||||
if scrobbleinfo.get('album_artists'):
|
||||
scrobbleinfo['album_artists'] = cla.parseArtists(scrobbleinfo['album_artists'])
|
||||
if scrobbleinfo.get("album_title"):
|
||||
scrobbleinfo['album_title'] = cla.parseAlbumtitle(scrobbleinfo['album_title'])
|
||||
scrobbleinfo['scrobble_time'] = scrobbleinfo.get('scrobble_time') or int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
|
||||
|
||||
# if we send [] as albumartists, it means various
|
||||
# if we send nothing, the scrobbler just doesnt support it and we assume track artists
|
||||
if ('album_title' in scrobbleinfo) and ('album_artists' not in scrobbleinfo):
|
||||
scrobbleinfo['album_artists'] = scrobbleinfo.get('track_artists')
|
||||
|
||||
# New plan, do this further down
|
||||
# NONE always means there is simply no info, so make a guess or whatever the options say
|
||||
# could use the track artists, but probably check if any album with the same name exists first
|
||||
# various artists always needs to be specified via []
|
||||
# TODO
|
||||
|
||||
# processed info to internal scrobble dict
|
||||
scrobbledict = {
|
||||
"time":scrobbleinfo.get('scrobble_time'),
|
||||
@ -139,7 +172,7 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None):
|
||||
"artists":scrobbleinfo.get('track_artists'),
|
||||
"title":scrobbleinfo.get('track_title'),
|
||||
"album":{
|
||||
"name":scrobbleinfo.get('album_name'),
|
||||
"albumtitle":scrobbleinfo.get('album_title'),
|
||||
"artists":scrobbleinfo.get('album_artists')
|
||||
},
|
||||
"length":scrobbleinfo.get('track_length')
|
||||
@ -148,11 +181,15 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None):
|
||||
"origin":f"client:{client}" if client else "generic",
|
||||
"extra":{
|
||||
k:scrobbleinfo[k] for k in scrobbleinfo if k not in
|
||||
['scrobble_time','track_artists','track_title','track_length','scrobble_duration']#,'album_name','album_artists']
|
||||
['scrobble_time','track_artists','track_title','track_length','scrobble_duration']#,'album_title','album_artists']
|
||||
# we still save album info in extra because the user might select majority album authority
|
||||
},
|
||||
"rawscrobble":rawscrobble
|
||||
}
|
||||
|
||||
if not scrobbledict["track"]["album"]["albumtitle"]:
|
||||
del scrobbledict["track"]["album"]
|
||||
|
||||
return scrobbledict
|
||||
|
||||
|
||||
@ -184,12 +221,23 @@ def edit_track(id,trackinfo):
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def edit_album(id,albuminfo):
|
||||
album = sqldb.get_album(id)
|
||||
log(f"Renaming {album['albumtitle']} to {albuminfo['albumtitle']}")
|
||||
result = sqldb.edit_album(id,albuminfo)
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def merge_artists(target_id,source_ids):
|
||||
sources = [sqldb.get_artist(id) for id in source_ids]
|
||||
target = sqldb.get_artist(target_id)
|
||||
log(f"Merging {sources} into {target}")
|
||||
result = sqldb.merge_artists(target_id,source_ids)
|
||||
sqldb.merge_artists(target_id,source_ids)
|
||||
result = {'sources':sources,'target':target}
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
@ -200,7 +248,20 @@ def merge_tracks(target_id,source_ids):
|
||||
sources = [sqldb.get_track(id) for id in source_ids]
|
||||
target = sqldb.get_track(target_id)
|
||||
log(f"Merging {sources} into {target}")
|
||||
result = sqldb.merge_tracks(target_id,source_ids)
|
||||
sqldb.merge_tracks(target_id,source_ids)
|
||||
result = {'sources':sources,'target':target}
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def merge_albums(target_id,source_ids):
|
||||
sources = [sqldb.get_album(id) for id in source_ids]
|
||||
target = sqldb.get_album(target_id)
|
||||
log(f"Merging {sources} into {target}")
|
||||
sqldb.merge_albums(target_id,source_ids)
|
||||
result = {'sources':sources,'target':target}
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
@ -208,27 +269,95 @@ def merge_tracks(target_id,source_ids):
|
||||
|
||||
|
||||
|
||||
@waitfordb
|
||||
def associate_albums_to_artist(target_id,source_ids,remove=False):
|
||||
sources = [sqldb.get_album(id) for id in source_ids]
|
||||
target = sqldb.get_artist(target_id)
|
||||
if remove:
|
||||
log(f"Removing {sources} from {target}")
|
||||
sqldb.remove_artists_from_albums(artist_ids=[target_id],album_ids=source_ids)
|
||||
else:
|
||||
log(f"Adding {sources} into {target}")
|
||||
sqldb.add_artists_to_albums(artist_ids=[target_id],album_ids=source_ids)
|
||||
result = {'sources':sources,'target':target}
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def associate_tracks_to_artist(target_id,source_ids,remove=False):
|
||||
sources = [sqldb.get_track(id) for id in source_ids]
|
||||
target = sqldb.get_artist(target_id)
|
||||
if remove:
|
||||
log(f"Removing {sources} from {target}")
|
||||
sqldb.remove_artists_from_tracks(artist_ids=[target_id],track_ids=source_ids)
|
||||
else:
|
||||
log(f"Adding {sources} into {target}")
|
||||
sqldb.add_artists_to_tracks(artist_ids=[target_id],track_ids=source_ids)
|
||||
result = {'sources':sources,'target':target}
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def associate_tracks_to_album(target_id,source_ids):
|
||||
# target_id None means remove from current album!
|
||||
sources = [sqldb.get_track(id) for id in source_ids]
|
||||
if target_id:
|
||||
target = sqldb.get_album(target_id)
|
||||
log(f"Adding {sources} into {target}")
|
||||
sqldb.add_tracks_to_albums({src:target_id for src in source_ids})
|
||||
else:
|
||||
sqldb.remove_album(source_ids)
|
||||
result = {'sources':sources,'target':target}
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@waitfordb
|
||||
def get_scrobbles(dbconn=None,**keys):
|
||||
(since,to) = keys.get('timerange').timestamps()
|
||||
if 'artist' in keys:
|
||||
result = sqldb.get_scrobbles_of_artist(artist=keys['artist'],since=since,to=to,dbconn=dbconn)
|
||||
elif 'track' in keys:
|
||||
result = sqldb.get_scrobbles_of_track(track=keys['track'],since=since,to=to,dbconn=dbconn)
|
||||
|
||||
reverse = keys.get('reverse',True) # comaptibility with old calls
|
||||
if keys.get('perpage',math.inf) is not math.inf:
|
||||
limit = (keys.get('page',0)+1) * keys.get('perpage',100)
|
||||
behead = keys.get('page',0) * keys.get('perpage',100)
|
||||
else:
|
||||
result = sqldb.get_scrobbles(since=since,to=to,dbconn=dbconn)
|
||||
limit = None
|
||||
behead = 0
|
||||
|
||||
|
||||
associated = keys.get('associated',False)
|
||||
if 'artist' in keys:
|
||||
result = sqldb.get_scrobbles_of_artist(artist=keys['artist'],since=since,to=to,associated=associated,limit=limit,reverse=reverse,dbconn=dbconn)
|
||||
elif 'track' in keys:
|
||||
result = sqldb.get_scrobbles_of_track(track=keys['track'],since=since,to=to,limit=limit,reverse=reverse,dbconn=dbconn)
|
||||
elif 'album' in keys:
|
||||
result = sqldb.get_scrobbles_of_album(album=keys['album'],since=since,to=to,limit=limit,reverse=reverse,dbconn=dbconn)
|
||||
else:
|
||||
result = sqldb.get_scrobbles(since=since,to=to,limit=limit,reverse=reverse,dbconn=dbconn)
|
||||
#return result[keys['page']*keys['perpage']:(keys['page']+1)*keys['perpage']]
|
||||
return list(reversed(result))
|
||||
|
||||
#print(result)
|
||||
|
||||
return list(result[behead:])
|
||||
|
||||
|
||||
@waitfordb
|
||||
def get_scrobbles_num(dbconn=None,**keys):
|
||||
(since,to) = keys.get('timerange').timestamps()
|
||||
associated = keys.get('associated',False)
|
||||
if 'artist' in keys:
|
||||
result = len(sqldb.get_scrobbles_of_artist(artist=keys['artist'],since=since,to=to,resolve_references=False,dbconn=dbconn))
|
||||
result = len(sqldb.get_scrobbles_of_artist(artist=keys['artist'],since=since,to=to,associated=associated,resolve_references=False,dbconn=dbconn))
|
||||
elif 'track' in keys:
|
||||
result = len(sqldb.get_scrobbles_of_track(track=keys['track'],since=since,to=to,resolve_references=False,dbconn=dbconn))
|
||||
elif 'album' in keys:
|
||||
result = len(sqldb.get_scrobbles_of_album(album=keys['album'],since=since,to=to,resolve_references=False,dbconn=dbconn))
|
||||
else:
|
||||
result = sqldb.get_scrobbles_num(since=since,to=to,dbconn=dbconn)
|
||||
return result
|
||||
@ -244,59 +373,184 @@ def get_tracks(dbconn=None,**keys):
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def get_artists(dbconn=None):
|
||||
return sqldb.get_artists(dbconn=dbconn)
|
||||
|
||||
|
||||
@waitfordb
|
||||
def get_charts_artists(dbconn=None,**keys):
|
||||
(since,to) = keys.get('timerange').timestamps()
|
||||
result = sqldb.count_scrobbles_by_artist(since=since,to=to,dbconn=dbconn)
|
||||
def get_albums(dbconn=None,**keys):
|
||||
if keys.get('artist') is None:
|
||||
result = sqldb.get_albums(dbconn=dbconn)
|
||||
else:
|
||||
result = sqldb.get_albums_of_artists([sqldb.get_artist_id(keys.get('artist'),create_new=False)],dbconn=dbconn)
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def get_charts_tracks(dbconn=None,**keys):
|
||||
def get_artists(dbconn=None):
|
||||
return sqldb.get_artists(dbconn=dbconn)
|
||||
|
||||
@waitfordb
|
||||
def get_albums_artist_appears_on(dbconn=None,**keys):
|
||||
|
||||
artist_id = sqldb.get_artist_id(keys['artist'],dbconn=dbconn)
|
||||
|
||||
albums = sqldb.get_albums_artists_appear_on([artist_id],dbconn=dbconn).get(artist_id) or []
|
||||
ownalbums = sqldb.get_albums_of_artists([artist_id],dbconn=dbconn).get(artist_id) or []
|
||||
|
||||
result = {
|
||||
"own_albums":ownalbums,
|
||||
"appears_on":[a for a in albums if a not in ownalbums]
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def get_tracks_without_album(dbconn=None,resolve_ids=True):
|
||||
return get_charts_tracks(album=None,timerange=alltime(),resolve_ids=resolve_ids,dbconn=dbconn)
|
||||
|
||||
@waitfordb
|
||||
def get_charts_artists(dbconn=None,resolve_ids=True,**keys):
|
||||
(since,to) = keys.get('timerange').timestamps()
|
||||
separate = keys.get('separate',False)
|
||||
result = sqldb.count_scrobbles_by_artist(since=since,to=to,resolve_ids=resolve_ids,associated=(not separate),dbconn=dbconn)
|
||||
|
||||
if resolve_ids:
|
||||
# only add associated info if we resolve
|
||||
map = sqldb.get_associated_artist_map(artist_ids=[entry['artist_id'] for entry in result if 'artist_id' in entry])
|
||||
for entry in result:
|
||||
if "artist_id" in entry:
|
||||
entry['associated_artists'] = map[entry['artist_id']]
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def get_charts_tracks(dbconn=None,resolve_ids=True,**keys):
|
||||
(since,to) = keys.get('timerange').timestamps()
|
||||
if 'artist' in keys:
|
||||
result = sqldb.count_scrobbles_by_track_of_artist(since=since,to=to,artist=keys['artist'],dbconn=dbconn)
|
||||
result = sqldb.count_scrobbles_by_track_of_artist(since=since,to=to,artist=keys['artist'],associated=keys.get('associated',False),resolve_ids=resolve_ids,dbconn=dbconn)
|
||||
elif 'album' in keys:
|
||||
result = sqldb.count_scrobbles_by_track_of_album(since=since,to=to,album=keys['album'],resolve_ids=resolve_ids,dbconn=dbconn)
|
||||
else:
|
||||
result = sqldb.count_scrobbles_by_track(since=since,to=to,dbconn=dbconn)
|
||||
result = sqldb.count_scrobbles_by_track(since=since,to=to,resolve_ids=resolve_ids,dbconn=dbconn)
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def get_charts_albums(dbconn=None,resolve_ids=True,only_own_albums=False,**keys):
|
||||
# TODO: different scrobble numbers for only own tracks on own album etc?
|
||||
(since,to) = keys.get('timerange').timestamps()
|
||||
|
||||
if 'artist' in keys:
|
||||
result = sqldb.count_scrobbles_by_album_combined(since=since,to=to,artist=keys['artist'],associated=keys.get('associated',False),resolve_ids=resolve_ids,dbconn=dbconn)
|
||||
if only_own_albums:
|
||||
# TODO: this doesnt take associated into account and doesnt change ranks
|
||||
result = [e for e in result if keys['artist'] in (e['album']['artists'] or [])]
|
||||
else:
|
||||
result = sqldb.count_scrobbles_by_album(since=since,to=to,resolve_ids=resolve_ids,dbconn=dbconn)
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def get_pulse(dbconn=None,**keys):
|
||||
|
||||
# amountkeys for pulse and performance aren't really necessary
|
||||
# since the amount of entries is completely determined by the time keys
|
||||
# but lets just include it in case
|
||||
reverse = keys.get('reverse',False)
|
||||
if keys.get('perpage',math.inf) is not math.inf:
|
||||
limit = (keys.get('page',0)+1) * keys.get('perpage',100)
|
||||
behead = keys.get('page',0) * keys.get('perpage',100)
|
||||
else:
|
||||
limit = math.inf
|
||||
behead = 0
|
||||
|
||||
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
||||
if reverse: rngs = reversed(list(rngs))
|
||||
results = []
|
||||
for rng in rngs:
|
||||
|
||||
# count down how many we need
|
||||
if limit==0:
|
||||
break
|
||||
limit -= 1
|
||||
|
||||
# skip prev pages
|
||||
if behead>0:
|
||||
behead -= 1
|
||||
continue
|
||||
|
||||
res = get_scrobbles_num(timerange=rng,**{k:keys[k] for k in keys if k != 'timerange'},dbconn=dbconn)
|
||||
results.append({"range":rng,"scrobbles":res})
|
||||
if keys.get('artist') and keys.get('associated',False):
|
||||
res_real = get_scrobbles_num(timerange=rng,**{k:keys[k] for k in keys if k not in ['timerange','associated']},associated=False,dbconn=dbconn)
|
||||
# this isnt really efficient, we could do that in one db call, but i dont wanna reorganize rn
|
||||
else:
|
||||
res_real = res
|
||||
results.append({"range":rng,"scrobbles":res,"real_scrobbles":res_real})
|
||||
|
||||
return results
|
||||
|
||||
@waitfordb
|
||||
def get_performance(dbconn=None,**keys):
|
||||
|
||||
# amountkeys for pulse and performance aren't really necessary
|
||||
# since the amount of entries is completely determined by the time keys
|
||||
# but lets just include it in case
|
||||
reverse = keys.get('reverse',False)
|
||||
if keys.get('perpage',math.inf) is not math.inf:
|
||||
limit = (keys.get('page',0)+1) * keys.get('perpage',100)
|
||||
behead = keys.get('page',0) * keys.get('perpage',100)
|
||||
else:
|
||||
limit = math.inf
|
||||
behead = 0
|
||||
|
||||
separate = keys.get('separate')
|
||||
|
||||
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
||||
if reverse: rngs = reversed(list(rngs))
|
||||
results = []
|
||||
|
||||
for rng in rngs:
|
||||
|
||||
# count down how many we need
|
||||
if limit==0:
|
||||
break
|
||||
limit -= 1
|
||||
|
||||
# skip prev pages
|
||||
if behead>0:
|
||||
behead -= 1
|
||||
continue
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if "track" in keys:
|
||||
track = sqldb.get_track(sqldb.get_track_id(keys['track'],dbconn=dbconn),dbconn=dbconn)
|
||||
charts = get_charts_tracks(timerange=rng,dbconn=dbconn)
|
||||
track_id = sqldb.get_track_id(keys['track'],create_new=False,dbconn=dbconn)
|
||||
if not track_id:
|
||||
raise exceptions.TrackDoesNotExist(keys['track'])
|
||||
#track = sqldb.get_track(track_id,dbconn=dbconn)
|
||||
charts = get_charts_tracks(timerange=rng,resolve_ids=False,dbconn=dbconn)
|
||||
rank = None
|
||||
for c in charts:
|
||||
if c["track"] == track:
|
||||
if c["track_id"] == track_id:
|
||||
rank = c["rank"]
|
||||
break
|
||||
elif "artist" in keys:
|
||||
artist = sqldb.get_artist(sqldb.get_artist_id(keys['artist'],dbconn=dbconn),dbconn=dbconn)
|
||||
artist_id = sqldb.get_artist_id(keys['artist'],create_new=False,dbconn=dbconn)
|
||||
if not artist_id:
|
||||
raise exceptions.ArtistDoesNotExist(keys['artist'])
|
||||
#artist = sqldb.get_artist(artist_id,dbconn=dbconn)
|
||||
# ^this is the most useless line in programming history
|
||||
# but I like consistency
|
||||
charts = get_charts_artists(timerange=rng,dbconn=dbconn)
|
||||
charts = get_charts_artists(timerange=rng,resolve_ids=False,separate=separate,dbconn=dbconn)
|
||||
rank = None
|
||||
for c in charts:
|
||||
if c["artist"] == artist:
|
||||
if c["artist_id"] == artist_id:
|
||||
rank = c["rank"]
|
||||
break
|
||||
elif "album" in keys:
|
||||
album_id = sqldb.get_album_id(keys['album'],create_new=False,dbconn=dbconn)
|
||||
if not album_id:
|
||||
raise exceptions.AlbumDoesNotExist(keys['album'])
|
||||
#album = sqldb.get_album(album_id,dbconn=dbconn)
|
||||
charts = get_charts_albums(timerange=rng,resolve_ids=False,dbconn=dbconn)
|
||||
rank = None
|
||||
for c in charts:
|
||||
if c["album_id"] == album_id:
|
||||
rank = c["rank"]
|
||||
break
|
||||
else:
|
||||
@ -308,15 +562,17 @@ def get_performance(dbconn=None,**keys):
|
||||
@waitfordb
|
||||
def get_top_artists(dbconn=None,**keys):
|
||||
|
||||
separate = keys.get('separate')
|
||||
|
||||
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
||||
results = []
|
||||
|
||||
for rng in rngs:
|
||||
try:
|
||||
res = get_charts_artists(timerange=rng,dbconn=dbconn)[0]
|
||||
results.append({"range":rng,"artist":res["artist"],"scrobbles":res["scrobbles"]})
|
||||
res = get_charts_artists(timerange=rng,separate=separate,dbconn=dbconn)[0]
|
||||
results.append({"range":rng,"artist":res["artist"],"scrobbles":res["scrobbles"],"real_scrobbles":res["real_scrobbles"],"associated_artists":sqldb.get_associated_artists(res["artist"])})
|
||||
except Exception:
|
||||
results.append({"range":rng,"artist":None,"scrobbles":0})
|
||||
results.append({"range":rng,"artist":None,"scrobbles":0,"real_scrobbles":0})
|
||||
|
||||
return results
|
||||
|
||||
@ -336,48 +592,112 @@ def get_top_tracks(dbconn=None,**keys):
|
||||
|
||||
return results
|
||||
|
||||
@waitfordb
|
||||
def get_top_albums(dbconn=None,**keys):
|
||||
|
||||
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
||||
results = []
|
||||
|
||||
for rng in rngs:
|
||||
try:
|
||||
res = get_charts_albums(timerange=rng,dbconn=dbconn)[0]
|
||||
results.append({"range":rng,"album":res["album"],"scrobbles":res["scrobbles"]})
|
||||
except Exception:
|
||||
results.append({"range":rng,"album":None,"scrobbles":0})
|
||||
|
||||
return results
|
||||
|
||||
@waitfordb
|
||||
def artist_info(dbconn=None,**keys):
|
||||
|
||||
artist = keys.get('artist')
|
||||
if artist is None: raise exceptions.MissingEntityParameter()
|
||||
|
||||
artist_id = sqldb.get_artist_id(artist,dbconn=dbconn)
|
||||
artist_id = sqldb.get_artist_id(artist,create_new=False,dbconn=dbconn)
|
||||
if not artist_id: raise exceptions.ArtistDoesNotExist(artist)
|
||||
|
||||
artist = sqldb.get_artist(artist_id,dbconn=dbconn)
|
||||
alltimecharts = get_charts_artists(timerange=alltime(),dbconn=dbconn)
|
||||
scrobbles = get_scrobbles_num(artist=artist,timerange=alltime(),dbconn=dbconn)
|
||||
#we cant take the scrobble number from the charts because that includes all countas scrobbles
|
||||
try:
|
||||
c = [e for e in alltimecharts if e["artist"] == artist][0]
|
||||
scrobbles = get_scrobbles_num(artist=artist,timerange=alltime(),dbconn=dbconn)
|
||||
albums = sqldb.get_albums_of_artists(set([artist_id]),dbconn=dbconn)
|
||||
isalbumartist = len(albums.get(artist_id,[]))>0
|
||||
|
||||
cert = None
|
||||
own_track_charts = get_charts_tracks(timerange=alltime(),resolve_ids=False,artist=artist,dbconn=dbconn)
|
||||
own_album_charts = get_charts_albums(timerange=alltime(),resolve_ids=True,artist=artist,dbconn=dbconn)
|
||||
# we resolve ids here which we don't need to. however, on the jinja page we make that same call
|
||||
# later again with resolve ids, so its a cache miss and it doubles page load time
|
||||
# TODO: find better solution
|
||||
if own_track_charts:
|
||||
c = own_track_charts[0]
|
||||
tscrobbles = c["scrobbles"]
|
||||
threshold_gold, threshold_platinum, threshold_diamond = malojaconfig["SCROBBLES_GOLD","SCROBBLES_PLATINUM","SCROBBLES_DIAMOND"]
|
||||
if tscrobbles >= threshold_diamond: cert = "diamond"
|
||||
elif tscrobbles >= threshold_platinum: cert = "platinum"
|
||||
elif tscrobbles >= threshold_gold: cert = "gold"
|
||||
if own_album_charts:
|
||||
c = own_album_charts[0]
|
||||
ascrobbles = c["scrobbles"]
|
||||
threshold_gold, threshold_platinum, threshold_diamond = malojaconfig["SCROBBLES_GOLD_ALBUM","SCROBBLES_PLATINUM_ALBUM","SCROBBLES_DIAMOND_ALBUM"]
|
||||
if ascrobbles >= threshold_diamond: cert = "diamond"
|
||||
elif ascrobbles >= threshold_platinum and cert != "diamond": cert = "platinum"
|
||||
elif ascrobbles >= threshold_gold and not cert: cert = "gold"
|
||||
|
||||
twk = thisweek()
|
||||
tyr = thisyear()
|
||||
|
||||
# base info for everyone
|
||||
result = {
|
||||
"artist":artist,
|
||||
"scrobbles":scrobbles,
|
||||
"id":artist_id,
|
||||
"isalbumartist":isalbumartist,
|
||||
"certification":cert,
|
||||
}
|
||||
|
||||
# check if credited to someone else
|
||||
parent_artists = sqldb.get_credited_artists(artist)
|
||||
if len(parent_artists) == 0:
|
||||
c = [e for e in alltimecharts if e["artist"] == artist]
|
||||
position = c[0]["rank"] if len(c) > 0 else None
|
||||
others = sqldb.get_associated_artists(artist,dbconn=dbconn)
|
||||
position = c["rank"]
|
||||
return {
|
||||
"artist":artist,
|
||||
"scrobbles":scrobbles,
|
||||
result.update({
|
||||
"position":position,
|
||||
"associated":others,
|
||||
"medals":{
|
||||
"gold": [year for year in cached.medals_artists if artist_id in cached.medals_artists[year]['gold']],
|
||||
"silver": [year for year in cached.medals_artists if artist_id in cached.medals_artists[year]['silver']],
|
||||
"bronze": [year for year in cached.medals_artists if artist_id in cached.medals_artists[year]['bronze']],
|
||||
"gold": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('artist_id') == artist_id) and (e.get('rank') == 1) for e in
|
||||
sqldb.count_scrobbles_by_artist(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)],
|
||||
"silver": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('artist_id') == artist_id) and (e.get('rank') == 2) for e in
|
||||
sqldb.count_scrobbles_by_artist(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)],
|
||||
"bronze": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('artist_id') == artist_id) and (e.get('rank') == 3) for e in
|
||||
sqldb.count_scrobbles_by_artist(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)]
|
||||
},
|
||||
"topweeks":len([e for e in cached.weekly_topartists if e == artist_id]),
|
||||
"id":artist_id
|
||||
}
|
||||
except Exception:
|
||||
# if the artist isnt in the charts, they are not being credited and we
|
||||
# need to show information about the credited one
|
||||
replaceartist = sqldb.get_credited_artists(artist)[0]
|
||||
"topweeks":len([
|
||||
week for week in ranges(step="week") if (week != twk) and any(
|
||||
(e.get('artist_id') == artist_id) and (e.get('rank') == 1) for e in
|
||||
sqldb.count_scrobbles_by_artist(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,associated=True,dbconn=dbconn)
|
||||
)
|
||||
# we don't need to check the whole thing, just until rank is lower, but... well, its a list comprehension
|
||||
])
|
||||
})
|
||||
|
||||
else:
|
||||
replaceartist = parent_artists[0]
|
||||
c = [e for e in alltimecharts if e["artist"] == replaceartist][0]
|
||||
position = c["rank"]
|
||||
return {
|
||||
"artist":artist,
|
||||
result.update({
|
||||
"replace":replaceartist,
|
||||
"scrobbles":scrobbles,
|
||||
"position":position,
|
||||
"id":artist_id
|
||||
}
|
||||
"position":position
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@ -387,12 +707,14 @@ def track_info(dbconn=None,**keys):
|
||||
track = keys.get('track')
|
||||
if track is None: raise exceptions.MissingEntityParameter()
|
||||
|
||||
track_id = sqldb.get_track_id(track,dbconn=dbconn)
|
||||
track_id = sqldb.get_track_id(track,create_new=False,dbconn=dbconn)
|
||||
if not track_id: raise exceptions.TrackDoesNotExist(track)
|
||||
|
||||
track = sqldb.get_track(track_id,dbconn=dbconn)
|
||||
alltimecharts = get_charts_tracks(timerange=alltime(),dbconn=dbconn)
|
||||
alltimecharts = get_charts_tracks(timerange=alltime(),resolve_ids=False,dbconn=dbconn)
|
||||
#scrobbles = get_scrobbles_num(track=track,timerange=alltime())
|
||||
|
||||
c = [e for e in alltimecharts if e["track"] == track][0]
|
||||
c = [e for e in alltimecharts if e["track_id"] == track_id][0]
|
||||
scrobbles = c["scrobbles"]
|
||||
position = c["rank"]
|
||||
cert = None
|
||||
@ -401,22 +723,129 @@ def track_info(dbconn=None,**keys):
|
||||
elif scrobbles >= threshold_platinum: cert = "platinum"
|
||||
elif scrobbles >= threshold_gold: cert = "gold"
|
||||
|
||||
twk = thisweek()
|
||||
tyr = thisyear()
|
||||
|
||||
return {
|
||||
"track":track,
|
||||
"scrobbles":scrobbles,
|
||||
"position":position,
|
||||
"medals":{
|
||||
"gold": [year for year in cached.medals_tracks if track_id in cached.medals_tracks[year]['gold']],
|
||||
"silver": [year for year in cached.medals_tracks if track_id in cached.medals_tracks[year]['silver']],
|
||||
"bronze": [year for year in cached.medals_tracks if track_id in cached.medals_tracks[year]['bronze']],
|
||||
"gold": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('track_id') == track_id) and (e.get('rank') == 1) for e in
|
||||
sqldb.count_scrobbles_by_track(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)],
|
||||
"silver": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('track_id') == track_id) and (e.get('rank') == 2) for e in
|
||||
sqldb.count_scrobbles_by_track(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)],
|
||||
"bronze": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('track_id') == track_id) and (e.get('rank') == 3) for e in
|
||||
sqldb.count_scrobbles_by_track(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)]
|
||||
},
|
||||
"certification":cert,
|
||||
"topweeks":len([e for e in cached.weekly_toptracks if e == track_id]),
|
||||
"topweeks":len([
|
||||
week for week in ranges(step="week") if (week != twk) and any(
|
||||
(e.get('track_id') == track_id) and (e.get('rank') == 1) for e in
|
||||
sqldb.count_scrobbles_by_track(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)
|
||||
]),
|
||||
"id":track_id
|
||||
}
|
||||
|
||||
|
||||
@waitfordb
|
||||
def album_info(dbconn=None,reduced=False,**keys):
|
||||
|
||||
album = keys.get('album')
|
||||
if album is None: raise exceptions.MissingEntityParameter()
|
||||
|
||||
album_id = sqldb.get_album_id(album,create_new=False,dbconn=dbconn)
|
||||
if not album_id: raise exceptions.AlbumDoesNotExist(album)
|
||||
|
||||
album = sqldb.get_album(album_id,dbconn=dbconn)
|
||||
|
||||
extrainfo = {}
|
||||
|
||||
if reduced:
|
||||
scrobbles = get_scrobbles_num(album=album,timerange=alltime())
|
||||
else:
|
||||
alltimecharts = get_charts_albums(timerange=alltime(),dbconn=dbconn)
|
||||
c = [e for e in alltimecharts if e["album"] == album][0]
|
||||
scrobbles = c["scrobbles"]
|
||||
position = c["rank"]
|
||||
extrainfo['position'] = position
|
||||
|
||||
cert = None
|
||||
threshold_gold, threshold_platinum, threshold_diamond = malojaconfig["SCROBBLES_GOLD_ALBUM","SCROBBLES_PLATINUM_ALBUM","SCROBBLES_DIAMOND_ALBUM"]
|
||||
if scrobbles >= threshold_diamond: cert = "diamond"
|
||||
elif scrobbles >= threshold_platinum: cert = "platinum"
|
||||
elif scrobbles >= threshold_gold: cert = "gold"
|
||||
|
||||
if reduced:
|
||||
pass
|
||||
else:
|
||||
twk = thisweek()
|
||||
tyr = thisyear()
|
||||
extrainfo.update({
|
||||
"medals":{
|
||||
"gold": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('album_id') == album_id) and (e.get('rank') == 1) for e in
|
||||
sqldb.count_scrobbles_by_album(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)],
|
||||
"silver": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('album_id') == album_id) and (e.get('rank') == 2) for e in
|
||||
sqldb.count_scrobbles_by_album(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)],
|
||||
"bronze": [year.desc() for year in ranges(step='year') if (year != tyr) and any(
|
||||
(e.get('album_id') == album_id) and (e.get('rank') == 3) for e in
|
||||
sqldb.count_scrobbles_by_album(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)]
|
||||
},
|
||||
"topweeks":len([
|
||||
week for week in ranges(step="week") if (week != twk) and any(
|
||||
(e.get('album_id') == album_id) and (e.get('rank') == 1) for e in
|
||||
sqldb.count_scrobbles_by_album(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
)
|
||||
])
|
||||
})
|
||||
|
||||
return {
|
||||
"album":album,
|
||||
"scrobbles":scrobbles,
|
||||
"certification":cert,
|
||||
"id":album_id,
|
||||
**extrainfo
|
||||
}
|
||||
|
||||
|
||||
|
||||
### TODO: FIND COOL ALGORITHM TO SELECT FEATURED STUFF
|
||||
@waitfordb
|
||||
def get_featured(dbconn=None):
|
||||
# temporary stand-in
|
||||
ranges = [
|
||||
MTRangeComposite(since=today().next(-14),to=today()),
|
||||
MTRangeComposite(since=thisweek().next(-12),to=thisweek()),
|
||||
MTRangeComposite(since=thisweek().next(-52),to=thisweek()),
|
||||
alltime()
|
||||
]
|
||||
funcs = {
|
||||
"artist": (get_charts_artists,{'associated':False}),
|
||||
"album": (get_charts_albums,{}),
|
||||
"track": (get_charts_tracks,{})
|
||||
}
|
||||
result = {t:None for t in funcs}
|
||||
|
||||
for entity_type in funcs:
|
||||
for r in ranges:
|
||||
func,kwargs = funcs[entity_type]
|
||||
chart = func(timerange=r,**kwargs)
|
||||
if chart:
|
||||
result[entity_type] = chart[0][entity_type]
|
||||
break
|
||||
return result
|
||||
|
||||
def get_predefined_rulesets(dbconn=None):
|
||||
validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
@ -464,6 +893,7 @@ def start_db():
|
||||
# Upgrade database
|
||||
from .. import upgrade
|
||||
upgrade.upgrade_db(sqldb.add_scrobbles)
|
||||
upgrade.parse_old_albums()
|
||||
|
||||
# Load temporary tables
|
||||
from . import associated
|
||||
@ -478,14 +908,15 @@ def start_db():
|
||||
except IndexError:
|
||||
register_scrobbletime(int(datetime.datetime.now().timestamp()))
|
||||
|
||||
|
||||
# create cached information
|
||||
cached.update_medals()
|
||||
cached.update_weekly()
|
||||
|
||||
dbstatus['complete'] = True
|
||||
|
||||
|
||||
# cache some stuff that we'll probably need
|
||||
with sqldb.engine.connect() as dbconn:
|
||||
with dbconn.begin():
|
||||
for week in ranges(step='week'):
|
||||
sqldb.count_scrobbles_by_artist(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,associated=True,dbconn=dbconn)
|
||||
sqldb.count_scrobbles_by_track(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
sqldb.count_scrobbles_by_album(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=dbconn)
|
||||
|
||||
|
||||
|
||||
@ -497,4 +928,7 @@ def db_search(query,type=None):
|
||||
results = sqldb.search_artist(query)
|
||||
if type=="TRACK":
|
||||
results = sqldb.search_track(query)
|
||||
if type=="ALBUM":
|
||||
results = sqldb.search_album(query)
|
||||
|
||||
return results
|
||||
|
@ -1,74 +0,0 @@
|
||||
# for information that is not authorative, but should be saved anyway because it
|
||||
# changes infrequently and DB access is expensive
|
||||
|
||||
from doreah.regular import runyearly, rundaily
|
||||
from .. import database
|
||||
from . import sqldb
|
||||
from .. import malojatime as mjt
|
||||
|
||||
|
||||
|
||||
medals_artists = {
|
||||
# year: {'gold':[],'silver':[],'bronze':[]}
|
||||
}
|
||||
medals_tracks = {
|
||||
# year: {'gold':[],'silver':[],'bronze':[]}
|
||||
}
|
||||
|
||||
weekly_topartists = []
|
||||
weekly_toptracks = []
|
||||
|
||||
@runyearly
|
||||
def update_medals():
|
||||
|
||||
global medals_artists, medals_tracks
|
||||
medals_artists.clear()
|
||||
medals_tracks.clear()
|
||||
|
||||
with sqldb.engine.begin() as conn:
|
||||
for year in mjt.ranges(step="year"):
|
||||
if year == mjt.thisyear(): break
|
||||
|
||||
charts_artists = sqldb.count_scrobbles_by_artist(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=conn)
|
||||
charts_tracks = sqldb.count_scrobbles_by_track(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=conn)
|
||||
|
||||
entry_artists = {'gold':[],'silver':[],'bronze':[]}
|
||||
entry_tracks = {'gold':[],'silver':[],'bronze':[]}
|
||||
medals_artists[year.desc()] = entry_artists
|
||||
medals_tracks[year.desc()] = entry_tracks
|
||||
|
||||
for entry in charts_artists:
|
||||
if entry['rank'] == 1: entry_artists['gold'].append(entry['artist_id'])
|
||||
elif entry['rank'] == 2: entry_artists['silver'].append(entry['artist_id'])
|
||||
elif entry['rank'] == 3: entry_artists['bronze'].append(entry['artist_id'])
|
||||
else: break
|
||||
for entry in charts_tracks:
|
||||
if entry['rank'] == 1: entry_tracks['gold'].append(entry['track_id'])
|
||||
elif entry['rank'] == 2: entry_tracks['silver'].append(entry['track_id'])
|
||||
elif entry['rank'] == 3: entry_tracks['bronze'].append(entry['track_id'])
|
||||
else: break
|
||||
|
||||
|
||||
|
||||
|
||||
@rundaily
|
||||
def update_weekly():
|
||||
|
||||
global weekly_topartists, weekly_toptracks
|
||||
weekly_topartists.clear()
|
||||
weekly_toptracks.clear()
|
||||
|
||||
with sqldb.engine.begin() as conn:
|
||||
for week in mjt.ranges(step="week"):
|
||||
if week == mjt.thisweek(): break
|
||||
|
||||
|
||||
charts_artists = sqldb.count_scrobbles_by_artist(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=conn)
|
||||
charts_tracks = sqldb.count_scrobbles_by_track(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=conn)
|
||||
|
||||
for entry in charts_artists:
|
||||
if entry['rank'] == 1: weekly_topartists.append(entry['artist_id'])
|
||||
else: break
|
||||
for entry in charts_tracks:
|
||||
if entry['rank'] == 1: weekly_toptracks.append(entry['track_id'])
|
||||
else: break
|
@ -10,7 +10,7 @@ from doreah.regular import runhourly
|
||||
from doreah.logging import log
|
||||
|
||||
from ..pkg_global.conf import malojaconfig
|
||||
|
||||
from . import no_aux_mode
|
||||
|
||||
|
||||
if malojaconfig['USE_GLOBAL_CACHE']:
|
||||
@ -21,6 +21,7 @@ if malojaconfig['USE_GLOBAL_CACHE']:
|
||||
|
||||
|
||||
@runhourly
|
||||
@no_aux_mode
|
||||
def maintenance():
|
||||
print_stats()
|
||||
trim_cache()
|
||||
@ -42,6 +43,7 @@ if malojaconfig['USE_GLOBAL_CACHE']:
|
||||
conn = None
|
||||
global hits, misses
|
||||
key = (serialize(args),serialize(kwargs), inner_func, kwargs.get("since"), kwargs.get("to"))
|
||||
# TODO: also factor in default values to get better chance of hits
|
||||
|
||||
try:
|
||||
return cache[key]
|
||||
@ -50,6 +52,8 @@ if malojaconfig['USE_GLOBAL_CACHE']:
|
||||
cache[key] = result
|
||||
return result
|
||||
|
||||
outer_func.__name__ = f"CACHD_{inner_func.__name__}"
|
||||
|
||||
return outer_func
|
||||
|
||||
|
||||
@ -80,22 +84,23 @@ if malojaconfig['USE_GLOBAL_CACHE']:
|
||||
|
||||
return outer_func
|
||||
|
||||
@no_aux_mode
|
||||
def invalidate_caches(scrobbletime=None):
|
||||
|
||||
cleared, kept = 0, 0
|
||||
for k in cache.keys():
|
||||
# VERY BIG TODO: differentiate between None as in 'unlimited timerange' and None as in 'time doesnt matter here'!
|
||||
if scrobbletime is None or (k[3] is None or scrobbletime >= k[3]) and (k[4] is None or scrobbletime <= k[4]):
|
||||
if scrobbletime is None or ((k[3] is None or scrobbletime >= k[3]) and (k[4] is None or scrobbletime <= k[4])):
|
||||
cleared += 1
|
||||
del cache[k]
|
||||
else:
|
||||
kept += 1
|
||||
log(f"Invalidated {cleared} of {cleared+kept} DB cache entries")
|
||||
|
||||
|
||||
@no_aux_mode
|
||||
def invalidate_entity_cache():
|
||||
entitycache.clear()
|
||||
|
||||
|
||||
def trim_cache():
|
||||
ramprct = psutil.virtual_memory().percent
|
||||
if ramprct > malojaconfig["DB_MAX_MEMORY"]:
|
||||
@ -132,7 +137,7 @@ else:
|
||||
def serialize(obj):
|
||||
try:
|
||||
return serialize(obj.hashable())
|
||||
except Exception:
|
||||
except AttributeError:
|
||||
try:
|
||||
return json.dumps(obj)
|
||||
except Exception:
|
||||
|
@ -11,12 +11,14 @@ class TrackExists(EntityExists):
|
||||
class ArtistExists(EntityExists):
|
||||
pass
|
||||
|
||||
class AlbumExists(EntityExists):
|
||||
pass
|
||||
|
||||
class DatabaseNotBuilt(HTTPError):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
status=503,
|
||||
body="The Maloja Database is being upgraded to Version 3. This could take quite a long time! (~ 2-5 minutes per 10 000 scrobbles)",
|
||||
body="The Maloja Database is being upgraded to support new Maloja features. This could take a while.",
|
||||
headers={"Retry-After":120}
|
||||
)
|
||||
|
||||
@ -27,3 +29,19 @@ class MissingScrobbleParameters(Exception):
|
||||
|
||||
class MissingEntityParameter(Exception):
|
||||
pass
|
||||
|
||||
class EntityDoesNotExist(HTTPError):
|
||||
entitytype = 'Entity'
|
||||
def __init__(self,entitydict):
|
||||
self.entitydict = entitydict
|
||||
super().__init__(
|
||||
status=404,
|
||||
body=f"The {self.entitytype} '{self.entitydict}' does not exist in the database."
|
||||
)
|
||||
|
||||
class ArtistDoesNotExist(EntityDoesNotExist):
|
||||
entitytype = 'Artist'
|
||||
class AlbumDoesNotExist(EntityDoesNotExist):
|
||||
entitytype = 'Album'
|
||||
class TrackDoesNotExist(EntityDoesNotExist):
|
||||
entitytype = 'Track'
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,32 +8,59 @@ from doreah.timing import Clock
|
||||
from ..pkg_global.conf import data_dir
|
||||
|
||||
|
||||
profiler = cProfile.Profile()
|
||||
|
||||
|
||||
FULL_PROFILE = False
|
||||
SINGLE_CALLS = False
|
||||
# only save the last single call instead of adding up all calls
|
||||
# of that function for more representative performance result
|
||||
|
||||
if not SINGLE_CALLS:
|
||||
profilers = {}
|
||||
times = {}
|
||||
|
||||
def profile(func):
|
||||
def newfunc(*args,**kwargs):
|
||||
|
||||
if FULL_PROFILE:
|
||||
benchmarkfolder = data_dir['logs']("benchmarks")
|
||||
os.makedirs(benchmarkfolder,exist_ok=True)
|
||||
realfunc = func
|
||||
while hasattr(realfunc, '__innerfunc__'):
|
||||
realfunc = realfunc.__innerfunc__
|
||||
|
||||
def newfunc(*args,**kwargs):
|
||||
|
||||
clock = Clock()
|
||||
clock.start()
|
||||
|
||||
if FULL_PROFILE:
|
||||
profiler.enable()
|
||||
result = func(*args,**kwargs)
|
||||
if FULL_PROFILE:
|
||||
profiler.disable()
|
||||
benchmarkfolder = data_dir['logs']("benchmarks")
|
||||
os.makedirs(benchmarkfolder,exist_ok=True)
|
||||
if SINGLE_CALLS:
|
||||
localprofiler = cProfile.Profile()
|
||||
else:
|
||||
localprofiler = profilers.setdefault(realfunc,cProfile.Profile())
|
||||
localprofiler.enable()
|
||||
|
||||
result = func(*args,**kwargs)
|
||||
|
||||
log(f"Executed {func.__name__} ({args}, {kwargs}) in {clock.stop():.2f}s",module="debug_performance")
|
||||
if FULL_PROFILE:
|
||||
localprofiler.disable()
|
||||
|
||||
seconds = clock.stop()
|
||||
|
||||
if not SINGLE_CALLS:
|
||||
times.setdefault(realfunc,[]).append(seconds)
|
||||
|
||||
if SINGLE_CALLS:
|
||||
log(f"Executed {realfunc.__name__} ({args}, {kwargs}) in {seconds:.3f}s",module="debug_performance")
|
||||
else:
|
||||
log(f"Executed {realfunc.__name__} ({args}, {kwargs}) in {seconds:.3f}s (Average: { sum(times[realfunc])/len(times[realfunc]):.3f}s)",module="debug_performance")
|
||||
|
||||
if FULL_PROFILE:
|
||||
targetfilename = os.path.join(benchmarkfolder,f"{realfunc.__name__}.stats")
|
||||
try:
|
||||
pstats.Stats(profiler).dump_stats(os.path.join(benchmarkfolder,f"{func.__name__}.stats"))
|
||||
pstats.Stats(localprofiler).dump_stats(targetfilename)
|
||||
log(f"Saved benchmark as {targetfilename}")
|
||||
except Exception:
|
||||
pass
|
||||
log(f"Failed to save benchmark as {targetfilename}")
|
||||
|
||||
return result
|
||||
|
||||
|
348
maloja/images.py
348
maloja/images.py
@ -12,195 +12,324 @@ import base64
|
||||
import requests
|
||||
import datauri
|
||||
import io
|
||||
from threading import Thread, Timer, BoundedSemaphore
|
||||
from threading import Lock
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import re
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import sqlalchemy as sql
|
||||
|
||||
|
||||
|
||||
MAX_RESOLVE_THREADS = 5
|
||||
MAX_SECONDS_TO_RESOLVE_REQUEST = 5
|
||||
|
||||
|
||||
# remove old db file (columns missing)
|
||||
try:
|
||||
os.remove(data_dir['cache']('images.sqlite'))
|
||||
except:
|
||||
pass
|
||||
|
||||
DB = {}
|
||||
engine = sql.create_engine(f"sqlite:///{data_dir['cache']('images.sqlite')}", echo = False)
|
||||
engine = sql.create_engine(f"sqlite:///{data_dir['cache']('imagecache.sqlite')}", echo = False)
|
||||
meta = sql.MetaData()
|
||||
|
||||
dblock = Lock()
|
||||
|
||||
DB['artists'] = sql.Table(
|
||||
'artists', meta,
|
||||
sql.Column('id',sql.Integer,primary_key=True),
|
||||
sql.Column('url',sql.String),
|
||||
sql.Column('expire',sql.Integer),
|
||||
sql.Column('raw',sql.String)
|
||||
# sql.Column('raw',sql.String)
|
||||
sql.Column('local',sql.Boolean),
|
||||
sql.Column('localproxyurl',sql.String)
|
||||
)
|
||||
DB['tracks'] = sql.Table(
|
||||
'tracks', meta,
|
||||
sql.Column('id',sql.Integer,primary_key=True),
|
||||
sql.Column('url',sql.String),
|
||||
sql.Column('expire',sql.Integer),
|
||||
sql.Column('raw',sql.String)
|
||||
# sql.Column('raw',sql.String)
|
||||
sql.Column('local',sql.Boolean),
|
||||
sql.Column('localproxyurl',sql.String)
|
||||
)
|
||||
DB['albums'] = sql.Table(
|
||||
'albums', meta,
|
||||
sql.Column('id',sql.Integer,primary_key=True),
|
||||
sql.Column('url',sql.String),
|
||||
sql.Column('expire',sql.Integer),
|
||||
# sql.Column('raw',sql.String)
|
||||
sql.Column('local',sql.Boolean),
|
||||
sql.Column('localproxyurl',sql.String)
|
||||
)
|
||||
|
||||
meta.create_all(engine)
|
||||
|
||||
def get_image_from_cache(id,table):
|
||||
def get_id_and_table(track_id=None,artist_id=None,album_id=None):
|
||||
if track_id:
|
||||
return track_id,'tracks'
|
||||
elif album_id:
|
||||
return album_id,'albums'
|
||||
elif artist_id:
|
||||
return artist_id,'artists'
|
||||
|
||||
def get_image_from_cache(track_id=None,artist_id=None,album_id=None):
|
||||
now = int(datetime.datetime.now().timestamp())
|
||||
entity_id, table = get_id_and_table(track_id=track_id,artist_id=artist_id,album_id=album_id)
|
||||
|
||||
with engine.begin() as conn:
|
||||
op = DB[table].select().where(
|
||||
DB[table].c.id==id,
|
||||
DB[table].c.id==entity_id,
|
||||
DB[table].c.expire>now
|
||||
)
|
||||
result = conn.execute(op).all()
|
||||
for row in result:
|
||||
if row.raw is not None:
|
||||
return {'type':'raw','value':row.raw}
|
||||
if row.local:
|
||||
return {'type':'localurl','value':row.url}
|
||||
elif row.localproxyurl:
|
||||
return {'type':'localurl','value':row.localproxyurl}
|
||||
else:
|
||||
return {'type':'url','value':row.url} # returns None as value if nonexistence cached
|
||||
return {'type':'url','value':row.url or None}
|
||||
# value none means nonexistence is cached
|
||||
# for some reason this can also be an empty string, so use or None here to unify
|
||||
return None # no cache entry
|
||||
|
||||
def set_image_in_cache(id,table,url):
|
||||
remove_image_from_cache(id,table)
|
||||
now = int(datetime.datetime.now().timestamp())
|
||||
if url is None:
|
||||
expire = now + (malojaconfig["CACHE_EXPIRE_NEGATIVE"] * 24 * 3600)
|
||||
else:
|
||||
expire = now + (malojaconfig["CACHE_EXPIRE_POSITIVE"] * 24 * 3600)
|
||||
def set_image_in_cache(url,track_id=None,artist_id=None,album_id=None,local=False):
|
||||
remove_image_from_cache(track_id=track_id,artist_id=artist_id,album_id=album_id)
|
||||
entity_id, table = get_id_and_table(track_id=track_id,artist_id=artist_id,album_id=album_id)
|
||||
|
||||
raw = dl_image(url)
|
||||
with dblock:
|
||||
now = int(datetime.datetime.now().timestamp())
|
||||
if url is None:
|
||||
expire = now + (malojaconfig["CACHE_EXPIRE_NEGATIVE"] * 24 * 3600)
|
||||
else:
|
||||
expire = now + (malojaconfig["CACHE_EXPIRE_POSITIVE"] * 24 * 3600)
|
||||
|
||||
with engine.begin() as conn:
|
||||
op = DB[table].insert().values(
|
||||
id=id,
|
||||
url=url,
|
||||
expire=expire,
|
||||
raw=raw
|
||||
)
|
||||
result = conn.execute(op)
|
||||
if not local and malojaconfig["PROXY_IMAGES"] and url is not None:
|
||||
localproxyurl = dl_image(url)
|
||||
else:
|
||||
localproxyurl = None
|
||||
|
||||
with engine.begin() as conn:
|
||||
op = DB[table].insert().values(
|
||||
id=entity_id,
|
||||
url=url,
|
||||
expire=expire,
|
||||
local=local,
|
||||
localproxyurl=localproxyurl
|
||||
)
|
||||
result = conn.execute(op)
|
||||
|
||||
def remove_image_from_cache(track_id=None,artist_id=None,album_id=None):
|
||||
entity_id, table = get_id_and_table(track_id=track_id,artist_id=artist_id,album_id=album_id)
|
||||
|
||||
with dblock:
|
||||
with engine.begin() as conn:
|
||||
op = DB[table].delete().where(
|
||||
DB[table].c.id==entity_id,
|
||||
).returning(
|
||||
DB[table].c.id,
|
||||
DB[table].c.localproxyurl
|
||||
)
|
||||
result = conn.execute(op).all()
|
||||
|
||||
for row in result:
|
||||
try:
|
||||
targetpath = data_dir['cache']('images',row.localproxyurl.split('/')[-1])
|
||||
os.remove(targetpath)
|
||||
except:
|
||||
pass
|
||||
|
||||
def remove_image_from_cache(id,table):
|
||||
with engine.begin() as conn:
|
||||
op = DB[table].delete().where(
|
||||
DB[table].c.id==id,
|
||||
)
|
||||
result = conn.execute(op)
|
||||
|
||||
def dl_image(url):
|
||||
if not malojaconfig["PROXY_IMAGES"]: return None
|
||||
if url is None: return None
|
||||
if url.startswith("/"): return None #local image
|
||||
try:
|
||||
r = requests.get(url)
|
||||
mime = r.headers.get('content-type') or 'image/jpg'
|
||||
data = io.BytesIO(r.content).read()
|
||||
uri = datauri.DataURI.make(mime,charset='ascii',base64=True,data=data)
|
||||
log(f"Downloaded {url} for local caching")
|
||||
return uri
|
||||
#uri = datauri.DataURI.make(mime,charset='ascii',base64=True,data=data)
|
||||
targetname = '%030x' % random.getrandbits(128)
|
||||
targetpath = data_dir['cache']('images',targetname)
|
||||
with open(targetpath,'wb') as fd:
|
||||
fd.write(data)
|
||||
return os.path.join("/cacheimages",targetname)
|
||||
except Exception:
|
||||
log(f"Image {url} could not be downloaded for local caching")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
resolver = ThreadPoolExecutor(max_workers=MAX_RESOLVE_THREADS,thread_name_prefix='image_resolve')
|
||||
|
||||
### getting images for any website embedding now ALWAYS returns just the generic link
|
||||
### even if we have already cached it, we will handle that on request
|
||||
def get_track_image(track=None,track_id=None):
|
||||
if track_id is None:
|
||||
track_id = database.sqldb.get_track_id(track)
|
||||
track_id = database.sqldb.get_track_id(track,create_new=False)
|
||||
|
||||
return f"/image?type=track&id={track_id}"
|
||||
if malojaconfig["USE_ALBUM_ARTWORK_FOR_TRACKS"]:
|
||||
if track is None:
|
||||
track = database.sqldb.get_track(track_id)
|
||||
if track.get("album"):
|
||||
album_id = database.sqldb.get_album_id(track["album"])
|
||||
return get_album_image(album_id=album_id)
|
||||
|
||||
resolver.submit(resolve_image,track_id=track_id)
|
||||
|
||||
return f"/image?track_id={track_id}"
|
||||
|
||||
def get_artist_image(artist=None,artist_id=None):
|
||||
if artist_id is None:
|
||||
artist_id = database.sqldb.get_artist_id(artist)
|
||||
artist_id = database.sqldb.get_artist_id(artist,create_new=False)
|
||||
|
||||
return f"/image?type=artist&id={artist_id}"
|
||||
resolver.submit(resolve_image,artist_id=artist_id)
|
||||
|
||||
return f"/image?artist_id={artist_id}"
|
||||
|
||||
def get_album_image(album=None,album_id=None):
|
||||
if album_id is None:
|
||||
album_id = database.sqldb.get_album_id(album,create_new=False)
|
||||
|
||||
resolver.submit(resolve_image,album_id=album_id)
|
||||
|
||||
return f"/image?album_id={album_id}"
|
||||
|
||||
|
||||
# this is to keep track of what is currently being resolved
|
||||
# so new requests know that they don't need to queue another resolve
|
||||
image_resolve_controller_lock = Lock()
|
||||
image_resolve_controller = {
|
||||
'artists':set(),
|
||||
'albums':set(),
|
||||
'tracks':set()
|
||||
}
|
||||
|
||||
# this function doesn't need to return any info
|
||||
# it runs async to do all the work that takes time and only needs to write the result
|
||||
# to the cache so the synchronous functions (http requests) can access it
|
||||
def resolve_image(artist_id=None,track_id=None,album_id=None):
|
||||
result = get_image_from_cache(artist_id=artist_id,track_id=track_id,album_id=album_id)
|
||||
if result is not None:
|
||||
# No need to do anything
|
||||
return
|
||||
|
||||
if artist_id:
|
||||
entitytype = 'artist'
|
||||
table = 'artists'
|
||||
getfunc, entity_id = database.sqldb.get_artist, artist_id
|
||||
elif track_id:
|
||||
entitytype = 'track'
|
||||
table = 'tracks'
|
||||
getfunc, entity_id = database.sqldb.get_track, track_id
|
||||
elif album_id:
|
||||
entitytype = 'album'
|
||||
table = 'albums'
|
||||
getfunc, entity_id = database.sqldb.get_album, album_id
|
||||
|
||||
|
||||
|
||||
resolve_semaphore = BoundedSemaphore(8)
|
||||
# is another thread already working on this?
|
||||
with image_resolve_controller_lock:
|
||||
if entity_id in image_resolve_controller[table]:
|
||||
return
|
||||
else:
|
||||
image_resolve_controller[table].add(entity_id)
|
||||
|
||||
|
||||
def resolve_track_image(track_id):
|
||||
|
||||
with resolve_semaphore:
|
||||
# check cache
|
||||
result = get_image_from_cache(track_id,'tracks')
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
track = database.sqldb.get_track(track_id)
|
||||
try:
|
||||
entity = getfunc(entity_id)
|
||||
|
||||
# local image
|
||||
if malojaconfig["USE_LOCAL_IMAGES"]:
|
||||
images = local_files(artists=track['artists'],title=track['title'])
|
||||
images = local_files(**{entitytype: entity})
|
||||
if len(images) != 0:
|
||||
result = random.choice(images)
|
||||
result = urllib.parse.quote(result)
|
||||
result = {'type':'url','value':result}
|
||||
set_image_in_cache(track_id,'tracks',result['value'])
|
||||
result = {'type':'localurl','value':result}
|
||||
set_image_in_cache(artist_id=artist_id,track_id=track_id,album_id=album_id,url=result['value'],local=True)
|
||||
return result
|
||||
|
||||
# third party
|
||||
result = thirdparty.get_image_track_all((track['artists'],track['title']))
|
||||
result = {'type':'url','value':result}
|
||||
set_image_in_cache(track_id,'tracks',result['value'])
|
||||
if artist_id:
|
||||
result = thirdparty.get_image_artist_all(entity)
|
||||
elif track_id:
|
||||
result = thirdparty.get_image_track_all((entity['artists'],entity['title']))
|
||||
elif album_id:
|
||||
result = thirdparty.get_image_album_all((entity['artists'],entity['albumtitle']))
|
||||
|
||||
return result
|
||||
result = {'type':'url','value':result or None}
|
||||
set_image_in_cache(artist_id=artist_id,track_id=track_id,album_id=album_id,url=result['value'])
|
||||
finally:
|
||||
with image_resolve_controller_lock:
|
||||
image_resolve_controller[table].remove(entity_id)
|
||||
|
||||
|
||||
def resolve_artist_image(artist_id):
|
||||
|
||||
with resolve_semaphore:
|
||||
# the actual http request for the full image
|
||||
def image_request(artist_id=None,track_id=None,album_id=None):
|
||||
|
||||
# because we use lazyload, we can allow our http requests to take a little while at least
|
||||
# not the full backend request, but a few seconds to give us time to fetch some images
|
||||
# because 503 retry-after doesn't seem to be honored
|
||||
attempt = 0
|
||||
while attempt < MAX_SECONDS_TO_RESOLVE_REQUEST:
|
||||
attempt += 1
|
||||
# check cache
|
||||
result = get_image_from_cache(artist_id,'artists')
|
||||
result = get_image_from_cache(artist_id=artist_id,track_id=track_id,album_id=album_id)
|
||||
if result is not None:
|
||||
# we got an entry, even if it's that there is no image (value None)
|
||||
if result['value'] is None:
|
||||
# use placeholder
|
||||
if malojaconfig["FANCY_PLACEHOLDER_ART"]:
|
||||
placeholder_url = "https://generative-placeholders.glitch.me/image?width=300&height=300&style="
|
||||
if artist_id:
|
||||
result['value'] = placeholder_url + f"tiles&colors={artist_id % 100}"
|
||||
if track_id:
|
||||
result['value'] = placeholder_url + f"triangles&colors={track_id % 100}"
|
||||
if album_id:
|
||||
result['value'] = placeholder_url + f"joy-division&colors={album_id % 100}"
|
||||
else:
|
||||
if artist_id:
|
||||
result['value'] = "/static/svg/placeholder_artist.svg"
|
||||
if track_id:
|
||||
result['value'] = "/static/svg/placeholder_track.svg"
|
||||
if album_id:
|
||||
result['value'] = "/static/svg/placeholder_album.svg"
|
||||
return result
|
||||
time.sleep(1)
|
||||
|
||||
artist = database.sqldb.get_artist(artist_id)
|
||||
# no entry, which means we're still working on it
|
||||
return {'type':'noimage','value':'wait'}
|
||||
|
||||
# local image
|
||||
if malojaconfig["USE_LOCAL_IMAGES"]:
|
||||
images = local_files(artist=artist)
|
||||
if len(images) != 0:
|
||||
result = random.choice(images)
|
||||
result = urllib.parse.quote(result)
|
||||
result = {'type':'url','value':result}
|
||||
set_image_in_cache(artist_id,'artists',result['value'])
|
||||
return result
|
||||
|
||||
# third party
|
||||
result = thirdparty.get_image_artist_all(artist)
|
||||
result = {'type':'url','value':result}
|
||||
set_image_in_cache(artist_id,'artists',result['value'])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# removes emojis and weird shit from names
|
||||
def clean(name):
|
||||
return "".join(c for c in name if c.isalnum() or c in []).strip()
|
||||
|
||||
def get_all_possible_filenames(artist=None,artists=None,title=None):
|
||||
# check if we're dealing with a track or artist, then clean up names
|
||||
# (only remove non-alphanumeric, allow korean and stuff)
|
||||
|
||||
if title is not None and artists is not None:
|
||||
track = True
|
||||
title, artists = clean(title), [clean(a) for a in artists]
|
||||
elif artist is not None:
|
||||
track = False
|
||||
# new and improved
|
||||
def get_all_possible_filenames(artist=None,track=None,album=None):
|
||||
if track:
|
||||
title, artists = clean(track['title']), [clean(a) for a in track['artists']]
|
||||
superfolder = "tracks/"
|
||||
elif album:
|
||||
title, artists = clean(album['albumtitle']), [clean(a) for a in album.get('artists') or []]
|
||||
superfolder = "albums/"
|
||||
elif artist:
|
||||
artist = clean(artist)
|
||||
else: return []
|
||||
|
||||
|
||||
superfolder = "tracks/" if track else "artists/"
|
||||
superfolder = "artists/"
|
||||
else:
|
||||
return []
|
||||
|
||||
filenames = []
|
||||
|
||||
if track:
|
||||
#unsafeartists = [artist.translate(None,"-_./\\") for artist in artists]
|
||||
if track or album:
|
||||
safeartists = [re.sub("[^a-zA-Z0-9]","",artist) for artist in artists]
|
||||
#unsafetitle = title.translate(None,"-_./\\")
|
||||
safetitle = re.sub("[^a-zA-Z0-9]","",title)
|
||||
|
||||
if len(artists) < 4:
|
||||
@ -210,7 +339,6 @@ def get_all_possible_filenames(artist=None,artists=None,title=None):
|
||||
unsafeperms = [sorted(artists)]
|
||||
safeperms = [sorted(safeartists)]
|
||||
|
||||
|
||||
for unsafeartistlist in unsafeperms:
|
||||
filename = "-".join(unsafeartistlist) + "_" + title
|
||||
if filename != "":
|
||||
@ -241,10 +369,11 @@ def get_all_possible_filenames(artist=None,artists=None,title=None):
|
||||
|
||||
return [superfolder + name for name in filenames]
|
||||
|
||||
def local_files(artist=None,artists=None,title=None):
|
||||
|
||||
def local_files(artist=None,album=None,track=None):
|
||||
|
||||
|
||||
filenames = get_all_possible_filenames(artist,artists,title)
|
||||
filenames = get_all_possible_filenames(artist=artist,album=album,track=track)
|
||||
|
||||
images = []
|
||||
|
||||
@ -271,13 +400,21 @@ class MalformedB64(Exception):
|
||||
pass
|
||||
|
||||
def set_image(b64,**keys):
|
||||
track = "title" in keys
|
||||
if track:
|
||||
entity = {'artists':keys['artists'],'title':keys['title']}
|
||||
id = database.sqldb.get_track_id(entity)
|
||||
else:
|
||||
entity = keys['artist']
|
||||
id = database.sqldb.get_artist_id(entity)
|
||||
if "title" in keys:
|
||||
entity = {"track":keys}
|
||||
id = database.sqldb.get_track_id(entity['track'])
|
||||
idkeys = {'track_id':id}
|
||||
dbtable = "tracks"
|
||||
elif "albumtitle" in keys:
|
||||
entity = {"album":keys}
|
||||
id = database.sqldb.get_album_id(entity['album'])
|
||||
idkeys = {'album_id':id}
|
||||
dbtable = "albums"
|
||||
elif "artist" in keys:
|
||||
entity = keys
|
||||
id = database.sqldb.get_artist_id(entity['artist'])
|
||||
idkeys = {'artist_id':id}
|
||||
dbtable = "artists"
|
||||
|
||||
log("Trying to set image, b64 string: " + str(b64[:30] + "..."),module="debug")
|
||||
|
||||
@ -288,13 +425,13 @@ def set_image(b64,**keys):
|
||||
type,b64 = match.groups()
|
||||
b64 = base64.b64decode(b64)
|
||||
filename = "webupload" + str(int(datetime.datetime.now().timestamp())) + "." + type
|
||||
for folder in get_all_possible_filenames(**keys):
|
||||
for folder in get_all_possible_filenames(**entity):
|
||||
if os.path.exists(data_dir['images'](folder)):
|
||||
with open(data_dir['images'](folder,filename),"wb") as f:
|
||||
f.write(b64)
|
||||
break
|
||||
else:
|
||||
folder = get_all_possible_filenames(**keys)[0]
|
||||
folder = get_all_possible_filenames(**entity)[0]
|
||||
os.makedirs(data_dir['images'](folder))
|
||||
with open(data_dir['images'](folder,filename),"wb") as f:
|
||||
f.write(b64)
|
||||
@ -303,7 +440,6 @@ def set_image(b64,**keys):
|
||||
log("Saved image as " + data_dir['images'](folder,filename),module="debug")
|
||||
|
||||
# set as current picture in rotation
|
||||
if track: set_image_in_cache(id,'tracks',os.path.join("/images",folder,filename))
|
||||
else: set_image_in_cache(id,'artists',os.path.join("/images",folder,filename))
|
||||
set_image_in_cache(**idkeys,url=os.path.join("/images",folder,filename),local=True)
|
||||
|
||||
return os.path.join("/images",folder,filename)
|
||||
|
@ -26,8 +26,6 @@ def update_jinja_environment():
|
||||
|
||||
JINJA_CONTEXT = {
|
||||
# maloja
|
||||
"db": database, #TODO: move these to connection manager as well
|
||||
#"dbp":dbp,
|
||||
"malojatime": malojatime,
|
||||
"images": images,
|
||||
"mlj_uri": malojauri,
|
||||
@ -72,6 +70,14 @@ def update_jinja_environment():
|
||||
{"identifier":"longtrailing","replacekeys":{"trail":3},"localisation":"Long Trailing"},
|
||||
{"identifier":"inert","replacekeys":{"trail":10},"localisation":"Inert","heavy":True},
|
||||
{"identifier":"cumulative","replacekeys":{"trail":math.inf},"localisation":"Cumulative","heavy":True}
|
||||
],
|
||||
"xassociated": [
|
||||
{"identifier":"include_associated","replacekeys":{"associated":True},"localisation":"Associated"},
|
||||
{"identifier":"exclude_associated","replacekeys":{"associated":False},"localisation":"Exclusive"}
|
||||
],
|
||||
"xseparate": [
|
||||
{"identifier":"count_combined","replacekeys":{"separate":False},"localisation":"Combined"},
|
||||
{"identifier":"count_separate","replacekeys":{"separate":True},"localisation":"Separate"}
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from datetime import timezone, timedelta, date, time, datetime
|
||||
from calendar import monthrange
|
||||
from os.path import commonprefix
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .pkg_global.conf import malojaconfig
|
||||
|
||||
@ -28,7 +28,7 @@ def register_scrobbletime(timestamp):
|
||||
|
||||
|
||||
# Generic Time Range
|
||||
class MTRangeGeneric:
|
||||
class MTRangeGeneric(ABC):
|
||||
|
||||
# despite the above, ranges that refer to the exact same real time range should evaluate as equal
|
||||
def __eq__(self,other):
|
||||
@ -68,6 +68,15 @@ class MTRangeGeneric:
|
||||
def __contains__(self,timestamp):
|
||||
return timestamp >= self.first_stamp() and timestamp <= self.last_stamp()
|
||||
|
||||
@abstractmethod
|
||||
def first_stamp(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def last_stamp(self):
|
||||
pass
|
||||
|
||||
|
||||
# Any range that has one defining base unit, whether week, year, etc.
|
||||
class MTRangeSingular(MTRangeGeneric):
|
||||
def fromstr(self):
|
||||
|
@ -4,7 +4,7 @@ import urllib
|
||||
import math
|
||||
|
||||
# this also sets defaults!
|
||||
def uri_to_internal(keys,forceTrack=False,forceArtist=False,api=False):
|
||||
def uri_to_internal(keys,accepted_entities=('artist','track','album'),forceTrack=False,forceArtist=False,forceAlbum=False,api=False):
|
||||
|
||||
# output:
|
||||
# 1 keys that define the filtered object like artist or track
|
||||
@ -12,14 +12,33 @@ def uri_to_internal(keys,forceTrack=False,forceArtist=False,api=False):
|
||||
# 3 keys that define interal time ranges
|
||||
# 4 keys that define amount limits
|
||||
|
||||
# if we force a type, that only means that the other types are not allowed
|
||||
# it could still have no type at all (any call that isn't filtering by entity)
|
||||
|
||||
if forceTrack: accepted_entities = ('track',)
|
||||
if forceArtist: accepted_entities = ('artist',)
|
||||
if forceAlbum: accepted_entities = ('album',)
|
||||
|
||||
# API backwards compatibility
|
||||
if "artist" in keys and "artist" not in accepted_entities:
|
||||
if "track" in accepted_entities:
|
||||
keys['trackartist'] = keys['artist']
|
||||
elif "album" in accepted_entities:
|
||||
keys['albumartist'] = keys['artist']
|
||||
|
||||
|
||||
# 1
|
||||
if "title" in keys and not forceArtist:
|
||||
filterkeys = {"track":{"artists":keys.getall("artist"),"title":keys.get("title")}}
|
||||
elif "artist" in keys and not forceTrack:
|
||||
filterkeys = {"artist":keys.get("artist")}
|
||||
if "associated" in keys: filterkeys["associated"] = True
|
||||
else:
|
||||
filterkeys = {}
|
||||
filterkeys = {}
|
||||
if "track" in accepted_entities and "title" in keys:
|
||||
filterkeys.update({"track":{"artists":keys.getall("trackartist"),"title":keys.get("title")}})
|
||||
if "artist" in accepted_entities and "artist" in keys:
|
||||
filterkeys.update({"artist": keys.get("artist"), "associated": (keys.get('associated', 'no').lower() == 'yes')})
|
||||
# associated is only used for filtering by artist, to indicate that we include associated artists
|
||||
# for actual artist charts, to show that we want to count them, use 'unified'
|
||||
if "album" in accepted_entities and "albumtitle" in keys:
|
||||
filterkeys.update({"album":{"artists":keys.getall("albumartist"),"albumtitle":keys.get("albumtitle")}})
|
||||
|
||||
|
||||
|
||||
# 2
|
||||
limitkeys = {}
|
||||
@ -51,11 +70,20 @@ def uri_to_internal(keys,forceTrack=False,forceArtist=False,api=False):
|
||||
#different max than the internal one! the user doesn't get to disable pagination
|
||||
if "page" in keys: amountkeys["page"] = int(keys["page"])
|
||||
if "perpage" in keys: amountkeys["perpage"] = int(keys["perpage"])
|
||||
|
||||
#amountkeys["reverse"] = (keys.get("reverse","no").lower() == "yes")
|
||||
# we have different defaults for different things, so here we need to actually pass true false or nothing dependent
|
||||
# on whether its specified
|
||||
if keys.get("reverse","").lower() == 'yes': amountkeys['reverse'] = True
|
||||
elif keys.get("reverse","").lower() == 'no': amountkeys['reverse'] = False
|
||||
|
||||
#5
|
||||
specialkeys = {}
|
||||
if "remote" in keys: specialkeys["remote"] = keys["remote"]
|
||||
#if "remote" in keys: specialkeys["remote"] = keys["remote"]
|
||||
specialkeys["separate"] = (keys.get('separate','no').lower() == 'yes')
|
||||
for k in keys:
|
||||
if k in ['remote','b64']:
|
||||
# TODO: better solution!
|
||||
specialkeys[k] = keys[k]
|
||||
|
||||
|
||||
return filterkeys, limitkeys, delimitkeys, amountkeys, specialkeys
|
||||
@ -80,10 +108,15 @@ def internal_to_uri(keys):
|
||||
if "artist" in keys:
|
||||
urikeys.append("artist",keys["artist"])
|
||||
if keys.get("associated"): urikeys.append("associated","yes")
|
||||
elif "track" in keys:
|
||||
if "track" in keys:
|
||||
for a in keys["track"]["artists"]:
|
||||
urikeys.append("artist",a)
|
||||
urikeys.append("trackartist",a)
|
||||
urikeys.append("title",keys["track"]["title"])
|
||||
if "album" in keys:
|
||||
for a in keys["album"].get("artists") or []:
|
||||
urikeys.append("albumartist",a)
|
||||
urikeys.append("albumtitle",keys["album"]["albumtitle"])
|
||||
|
||||
|
||||
#time
|
||||
if "timerange" in keys:
|
||||
@ -116,6 +149,11 @@ def internal_to_uri(keys):
|
||||
urikeys.append("page",str(keys["page"]))
|
||||
if "perpage" in keys:
|
||||
urikeys.append("perpage",str(keys["perpage"]))
|
||||
if "reverse" in keys:
|
||||
urikeys.append("reverse","yes" if keys['reverse'] else "no")
|
||||
|
||||
if keys.get("separate",False):
|
||||
urikeys.append("separate","yes")
|
||||
|
||||
|
||||
return urikeys
|
||||
|
@ -6,6 +6,8 @@ from doreah.configuration import types as tp
|
||||
from ..__pkginfo__ import VERSION
|
||||
|
||||
|
||||
# this mode specifies whether we run some auxiliary task instead of the main server
|
||||
AUX_MODE = True
|
||||
|
||||
|
||||
# if DATA_DIRECTORY is specified, this is the directory to use for EVERYTHING, no matter what
|
||||
@ -148,14 +150,17 @@ malojaconfig = Configuration(
|
||||
"Technical":{
|
||||
"cache_expire_positive":(tp.Integer(), "Image Cache Expiration", 60, "Days until images are refetched"),
|
||||
"cache_expire_negative":(tp.Integer(), "Image Cache Negative Expiration", 5, "Days until failed image fetches are reattempted"),
|
||||
"db_max_memory":(tp.Integer(min=0,max=100), "RAM Percentage soft limit", 50, "RAM Usage in percent at which Maloja should no longer increase its database cache."),
|
||||
"db_max_memory":(tp.Integer(min=0,max=100), "RAM Percentage soft limit", 70, "RAM Usage in percent at which Maloja should no longer increase its database cache."),
|
||||
"use_request_cache":(tp.Boolean(), "Use request-local DB Cache", False),
|
||||
"use_global_cache":(tp.Boolean(), "Use global DB Cache", True)
|
||||
"use_global_cache":(tp.Boolean(), "Use global DB Cache", True, "This is vital for Maloja's performance. Do not disable this unless you have a strong reason to.")
|
||||
},
|
||||
"Fluff":{
|
||||
"scrobbles_gold":(tp.Integer(), "Scrobbles for Gold", 250, "How many scrobbles a track needs to be considered 'Gold' status"),
|
||||
"scrobbles_platinum":(tp.Integer(), "Scrobbles for Platinum", 500, "How many scrobbles a track needs to be considered 'Platinum' status"),
|
||||
"scrobbles_diamond":(tp.Integer(), "Scrobbles for Diamond", 1000, "How many scrobbles a track needs to be considered 'Diamond' status"),
|
||||
"scrobbles_gold":(tp.Integer(), "Scrobbles for Gold (Track)", 250, "How many scrobbles a track needs to be considered 'Gold' status"),
|
||||
"scrobbles_platinum":(tp.Integer(), "Scrobbles for Platinum (Track)",500, "How many scrobbles a track needs to be considered 'Platinum' status"),
|
||||
"scrobbles_diamond":(tp.Integer(), "Scrobbles for Diamond (Track)",1000, "How many scrobbles a track needs to be considered 'Diamond' status"),
|
||||
"scrobbles_gold_album":(tp.Integer(), "Scrobbles for Gold (Album)", 500, "How many scrobbles an album needs to be considered 'Gold' status"),
|
||||
"scrobbles_platinum_album":(tp.Integer(), "Scrobbles for Platinum (Album)",750, "How many scrobbles an album needs to be considered 'Platinum' status"),
|
||||
"scrobbles_diamond_album":(tp.Integer(), "Scrobbles for Diamond (Album)",1500, "How many scrobbles an album needs to be considered 'Diamond' status"),
|
||||
"name":(tp.String(), "Name", "Generic Maloja User")
|
||||
},
|
||||
"Third Party Services":{
|
||||
@ -177,6 +182,7 @@ malojaconfig = Configuration(
|
||||
|
||||
},
|
||||
"Database":{
|
||||
"album_information_trust":(tp.Choice({'first':"First",'last':"Last",'majority':"Majority"}), "Album Information Authority","first", "Whether to trust the first album information that is sent with a track or update every time a different album is sent"),
|
||||
"invalid_artists":(tp.Set(tp.String()), "Invalid Artists", ["[Unknown Artist]","Unknown Artist","Spotify"], "Artists that should be discarded immediately"),
|
||||
"remove_from_title":(tp.Set(tp.String()), "Remove from Title", ["(Original Mix)","(Radio Edit)","(Album Version)","(Explicit Version)","(Bonus Track)"], "Phrases that should be removed from song titles"),
|
||||
"delimiters_feat":(tp.Set(tp.String()), "Featuring Delimiters", ["ft.","ft","feat.","feat","featuring"], "Delimiters used for extra artists, even when in the title field"),
|
||||
@ -186,11 +192,14 @@ malojaconfig = Configuration(
|
||||
"parse_remix_artists":(tp.Boolean(), "Parse Remix Artists", False)
|
||||
},
|
||||
"Web Interface":{
|
||||
"default_range_charts_artists":(tp.Choice({'alltime':'All Time','year':'Year','month':"Month",'week':'Week'}), "Default Range Artist Charts", "year"),
|
||||
"default_range_charts_tracks":(tp.Choice({'alltime':'All Time','year':'Year','month':"Month",'week':'Week'}), "Default Range Track Charts", "year"),
|
||||
"default_range_startpage":(tp.Choice({'alltime':'All Time','year':'Year','month':"Month",'week':'Week'}), "Default Range for Startpage Stats", "year"),
|
||||
"default_step_pulse":(tp.Choice({'year':'Year','month':"Month",'week':'Week','day':'Day'}), "Default Pulse Step", "month"),
|
||||
"charts_display_tiles":(tp.Boolean(), "Display Chart Tiles", False),
|
||||
"album_showcase":(tp.Boolean(), "Display Album Showcase", True, "Display a graphical album showcase for artist overview pages instead of a chart list"),
|
||||
"display_art_icons":(tp.Boolean(), "Display Album/Artist Icons", True),
|
||||
"default_album_artist":(tp.String(), "Default Albumartist", "Various Artists"),
|
||||
"use_album_artwork_for_tracks":(tp.Boolean(), "Use Album Artwork for tracks", True),
|
||||
"fancy_placeholder_art":(tp.Boolean(), "Use fancy placeholder artwork",True),
|
||||
"discourage_cpu_heavy_stats":(tp.Boolean(), "Discourage CPU-heavy stats", False, "Prevent visitors from mindlessly clicking on CPU-heavy options. Does not actually disable them for malicious actors!"),
|
||||
"use_local_images":(tp.Boolean(), "Use Local Images", True),
|
||||
#"local_image_rotate":(tp.Integer(), "Local Image Rotate", 3600),
|
||||
@ -297,15 +306,6 @@ data_dir = {
|
||||
|
||||
|
||||
|
||||
### write down the last ran version
|
||||
with open(pthj(dir_settings['state'],".lastmalojaversion"),"w") as filed:
|
||||
filed.write(VERSION)
|
||||
filed.write("\n")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### DOREAH CONFIGURATION
|
||||
|
||||
from doreah import config
|
||||
@ -331,7 +331,8 @@ config(
|
||||
|
||||
custom_css_files = [f for f in os.listdir(data_dir['css']()) if f.lower().endswith('.css')]
|
||||
|
||||
|
||||
from ..database.sqldb import set_maloja_info
|
||||
set_maloja_info({'last_run_version':VERSION})
|
||||
|
||||
# what the fuck did i just write
|
||||
# this spaghetti file is proudly sponsored by the rice crackers i'm eating at the
|
||||
|
@ -1,3 +1,4 @@
|
||||
from .import_scrobbles import import_scrobbles
|
||||
from .backup import backup
|
||||
from .export import export # read that line out loud
|
||||
from .parse_albums import parse_albums
|
||||
|
@ -49,7 +49,7 @@ def import_scrobbles(inputf):
|
||||
typeid,typedesc = "spotify","Spotify"
|
||||
importfunc = parse_spotify_lite_legacy
|
||||
|
||||
elif re.match(r"maloja_export_[0-9]+\.json",filename):
|
||||
elif re.match(r"maloja_export[_0-9]*\.json",filename):
|
||||
typeid,typedesc = "maloja","Maloja"
|
||||
importfunc = parse_maloja
|
||||
|
||||
@ -88,10 +88,6 @@ def import_scrobbles(inputf):
|
||||
|
||||
# extra info
|
||||
extrainfo = {}
|
||||
if scrobble.get('album_name'): extrainfo['album_name'] = scrobble['album_name']
|
||||
if scrobble.get('album_artist'): extrainfo['album_artist'] = scrobble['album_artist']
|
||||
# saving this in the scrobble instead of the track because for now it's not meant
|
||||
# to be authorative information, just payload of the scrobble
|
||||
|
||||
scrobblebuffer.append({
|
||||
"time":scrobble['scrobble_time'],
|
||||
@ -99,6 +95,11 @@ def import_scrobbles(inputf):
|
||||
"artists":scrobble['track_artists'],
|
||||
"title":scrobble['track_title'],
|
||||
"length":scrobble['track_length'],
|
||||
"album":{
|
||||
"albumtitle":scrobble.get('album_name') or None,
|
||||
"artists":scrobble.get('album_artists') or scrobble['track_artists'] or None
|
||||
# TODO: use same heuristics as with parsing to determine album?
|
||||
} if scrobble.get('album_name') else None
|
||||
},
|
||||
"duration":scrobble['scrobble_duration'],
|
||||
"origin":"import:" + typeid,
|
||||
@ -441,7 +442,8 @@ def parse_maloja(inputf):
|
||||
'track_title': s['track']['title'],
|
||||
'track_artists': s['track']['artists'],
|
||||
'track_length': s['track']['length'],
|
||||
'album_name': s['track'].get('album',{}).get('name',''),
|
||||
'album_name': s['track'].get('album',{}).get('albumtitle','') if s['track'].get('album') is not None else '',
|
||||
'album_artists': s['track'].get('album',{}).get('artists',None) if s['track'].get('album') is not None else '',
|
||||
'scrobble_time': s['time'],
|
||||
'scrobble_duration': s['duration']
|
||||
},'')
|
||||
|
108
maloja/proccontrol/tasks/parse_albums.py
Normal file
108
maloja/proccontrol/tasks/parse_albums.py
Normal file
@ -0,0 +1,108 @@
|
||||
from doreah.io import col
|
||||
|
||||
def parse_albums(strategy=None,prefer_existing=False):
|
||||
|
||||
if strategy not in ("track","none","all","majority","most"):
|
||||
print("""
|
||||
Please specify your album parsing strategy:
|
||||
|
||||
--strategy Specify what strategy to use when the scrobble contains
|
||||
no information about album artists.
|
||||
track Take the track artists. This can lead to
|
||||
separate albums being created for compilation
|
||||
albums or albums that have collaboration tracks.
|
||||
none Merge all albums with the same name and assign
|
||||
'Various Artists' as the album artist.
|
||||
all Merge all albums with the same name and assign
|
||||
every artist that appears on the album as an album
|
||||
artist.
|
||||
majority Merge all albums with the same name and assign
|
||||
artists that appear in at least half the tracks
|
||||
of the album as album artists. [RECOMMENDED]
|
||||
most Merge all albums with the same name and assign
|
||||
the artist that appears most on the album as album
|
||||
artist.
|
||||
--prefer_existing If an album with the same name already exists, use it
|
||||
without further examination of track artists.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
|
||||
from ...database.sqldb import guess_albums, get_album_id, add_track_to_album
|
||||
|
||||
print("Parsing album information...")
|
||||
result = guess_albums()
|
||||
|
||||
result = {track_id:result[track_id] for track_id in result if result[track_id]["assigned"]}
|
||||
print("Found",col['yellow'](len(result)),"Tracks to assign albums to")
|
||||
|
||||
result_authorative = {track_id:result[track_id] for track_id in result if result[track_id]["assigned"]["artists"]}
|
||||
result_guesswork = {track_id:result[track_id] for track_id in result if not result[track_id]["assigned"]["artists"]}
|
||||
|
||||
i = 0
|
||||
|
||||
def countup(i):
|
||||
i+=1
|
||||
if (i % 100) == 0:
|
||||
print(f"Added album information for {i} of {len(result)} tracks...")
|
||||
return i
|
||||
|
||||
for track_id in result_authorative:
|
||||
albuminfo = result[track_id]['assigned']
|
||||
album_id = get_album_id(albuminfo)
|
||||
add_track_to_album(track_id,album_id)
|
||||
i=countup(i)
|
||||
|
||||
albums = {}
|
||||
for track_id in result_guesswork:
|
||||
albuminfo = result[track_id]['assigned']
|
||||
|
||||
# check if already exists
|
||||
if prefer_existing:
|
||||
album_id = get_album_id(albuminfo,ignore_albumartists=True,create_new=False)
|
||||
if album_id:
|
||||
add_track_to_album(track_id,album_id)
|
||||
i=countup(i)
|
||||
continue
|
||||
|
||||
if strategy == 'track':
|
||||
albuminfo['artists'] = result[track_id]['guess_artists']
|
||||
album_id = get_album_id(albuminfo)
|
||||
add_track_to_album(track_id,album_id)
|
||||
i=countup(i)
|
||||
continue
|
||||
|
||||
if strategy == 'none':
|
||||
albuminfo['artists'] = []
|
||||
album_id = get_album_id(albuminfo)
|
||||
add_track_to_album(track_id,album_id)
|
||||
i=countup(i)
|
||||
continue
|
||||
|
||||
if strategy in ['all','majority','most']:
|
||||
cleantitle = albuminfo['albumtitle'].lower()
|
||||
albums.setdefault(cleantitle,{'track_ids':[],'artists':{},'title':albuminfo['albumtitle']})
|
||||
albums[cleantitle]['track_ids'].append(track_id)
|
||||
for a in result[track_id]['guess_artists']:
|
||||
albums[cleantitle]['artists'].setdefault(a,0)
|
||||
albums[cleantitle]['artists'][a] += 1
|
||||
|
||||
|
||||
for cleantitle in albums:
|
||||
artistoptions = albums[cleantitle]['artists']
|
||||
track_ids = albums[cleantitle]['track_ids']
|
||||
realtitle = albums[cleantitle]['title']
|
||||
if strategy == 'all':
|
||||
artists = [a for a in artistoptions]
|
||||
elif strategy == 'majority':
|
||||
artists = [a for a in artistoptions if artistoptions[a] >= (len(track_ids) / 2)]
|
||||
elif strategy == 'most':
|
||||
artists = [max(artistoptions,key=artistoptions.get)]
|
||||
|
||||
for track_id in track_ids:
|
||||
album_id = get_album_id({'albumtitle':realtitle,'artists':artists})
|
||||
add_track_to_album(track_id,album_id)
|
||||
i=countup(i)
|
||||
|
||||
print(col['lawngreen']("Done!"))
|
@ -1,9 +1,7 @@
|
||||
# technical
|
||||
import sys
|
||||
import os
|
||||
from threading import Thread
|
||||
from importlib import resources
|
||||
import datauri
|
||||
import time
|
||||
|
||||
|
||||
@ -19,9 +17,10 @@ from doreah import auth
|
||||
# rest of the project
|
||||
from . import database
|
||||
from .database.jinjaview import JinjaDBConnection
|
||||
from .images import resolve_track_image, resolve_artist_image
|
||||
from .images import image_request
|
||||
from .malojauri import uri_to_internal, remove_identical
|
||||
from .pkg_global.conf import malojaconfig, data_dir
|
||||
from .pkg_global import conf
|
||||
from .jinjaenv.context import jinja_environment
|
||||
from .apis import init_apis, apikeystore
|
||||
|
||||
@ -120,20 +119,14 @@ def deprecated_api(pth):
|
||||
@webserver.route("/image")
|
||||
def dynamic_image():
|
||||
keys = FormsDict.decode(request.query)
|
||||
if keys['type'] == 'track':
|
||||
result = resolve_track_image(keys['id'])
|
||||
elif keys['type'] == 'artist':
|
||||
result = resolve_artist_image(keys['id'])
|
||||
result = image_request(**{k:int(keys[k]) for k in keys})
|
||||
|
||||
if result is None or result['value'] in [None,'']:
|
||||
return ""
|
||||
if result['type'] == 'raw':
|
||||
# data uris are directly served as image because a redirect to a data uri
|
||||
# doesnt work
|
||||
duri = datauri.DataURI(result['value'])
|
||||
response.content_type = duri.mimetype
|
||||
return duri.data
|
||||
if result['type'] == 'url':
|
||||
if result['type'] == 'noimage' and result['value'] == 'wait':
|
||||
# still being worked on
|
||||
response.status = 202
|
||||
response.set_header('Retry-After',15)
|
||||
return
|
||||
if result['type'] in ('url','localurl'):
|
||||
redirect(result['value'],307)
|
||||
|
||||
@webserver.route("/images/<pth:re:.*\\.jpeg>")
|
||||
@ -160,6 +153,9 @@ def static_image(pth):
|
||||
resp.set_header("Content-Type", "image/" + ext)
|
||||
return resp
|
||||
|
||||
@webserver.route("/cacheimages/<uuid>")
|
||||
def static_proxied_image(uuid):
|
||||
return static_file(uuid,root=data_dir['cache']('images'))
|
||||
|
||||
@webserver.route("/login")
|
||||
def login():
|
||||
@ -218,8 +214,8 @@ def jinja_page(name):
|
||||
res = template.render(**loc_context)
|
||||
except TemplateNotFound:
|
||||
abort(404,f"Not found: '{name}'")
|
||||
except (ValueError, IndexError):
|
||||
abort(404,"This Artist or Track does not exist")
|
||||
#except (ValueError, IndexError):
|
||||
# abort(404,"This Artist or Track does not exist")
|
||||
|
||||
if malojaconfig["DEV_MODE"]: jinja_environment.cache.clear()
|
||||
|
||||
@ -283,6 +279,8 @@ logging.getLogger().addHandler(WaitressLogHandler())
|
||||
|
||||
|
||||
def run_server():
|
||||
conf.AUX_MODE = False
|
||||
|
||||
log("Starting up Maloja server...")
|
||||
|
||||
## start database
|
||||
|
@ -1,8 +1,10 @@
|
||||
import os
|
||||
|
||||
from importlib import resources
|
||||
from distutils import dir_util
|
||||
|
||||
try:
|
||||
from setuptools import distutils
|
||||
except ImportError:
|
||||
import distutils
|
||||
from doreah.io import col, ask, prompt
|
||||
from doreah import auth
|
||||
|
||||
@ -23,7 +25,7 @@ ext_apikeys = [
|
||||
def copy_initial_local_files():
|
||||
with resources.files("maloja") / 'data_files' as folder:
|
||||
for cat in dir_settings:
|
||||
dir_util.copy_tree(os.path.join(folder,cat),dir_settings[cat],update=False)
|
||||
distutils.dir_util.copy_tree(os.path.join(folder,cat),dir_settings[cat],update=False)
|
||||
|
||||
charset = list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
def randomstring(length=32):
|
||||
|
69
maloja/thirdparty/__init__.py
vendored
69
maloja/thirdparty/__init__.py
vendored
@ -10,6 +10,7 @@ import xml.etree.ElementTree as ElementTree
|
||||
import json
|
||||
import urllib.parse, urllib.request
|
||||
import base64
|
||||
import time
|
||||
from doreah.logging import log
|
||||
from threading import BoundedSemaphore
|
||||
|
||||
@ -23,6 +24,14 @@ services = {
|
||||
"metadata":[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
class InvalidResponse(Exception):
|
||||
"""Invalid Response from Third Party"""
|
||||
|
||||
class RateLimitExceeded(Exception):
|
||||
"""Rate Limit exceeded"""
|
||||
|
||||
# have a limited number of worker threads so we don't completely hog the cpu with
|
||||
# these requests. they are mostly network bound, so python will happily open up 200 new
|
||||
# requests and then when all the responses come in we suddenly can't load pages anymore
|
||||
@ -44,26 +53,37 @@ def get_image_track_all(track):
|
||||
for service in services["metadata"]:
|
||||
try:
|
||||
res = service.get_image_track(track)
|
||||
if res is not None:
|
||||
if res:
|
||||
log("Got track image for " + str(track) + " from " + service.name)
|
||||
return res
|
||||
else:
|
||||
log("Could not get track image for " + str(track) + " from " + service.name)
|
||||
log(f"Could not get track image for {track} from {service.name}")
|
||||
except Exception as e:
|
||||
log("Error getting track image from " + service.name + ": " + repr(e))
|
||||
log(f"Error getting track image from {service.name}: {e.__doc__}")
|
||||
def get_image_artist_all(artist):
|
||||
with thirdpartylock:
|
||||
for service in services["metadata"]:
|
||||
try:
|
||||
res = service.get_image_artist(artist)
|
||||
if res is not None:
|
||||
if res:
|
||||
log("Got artist image for " + str(artist) + " from " + service.name)
|
||||
return res
|
||||
else:
|
||||
log("Could not get artist image for " + str(artist) + " from " + service.name)
|
||||
log(f"Could not get artist image for {artist} from {service.name}")
|
||||
except Exception as e:
|
||||
log("Error getting artist image from " + service.name + ": " + repr(e))
|
||||
|
||||
log(f"Error getting artist image from {service.name}: {e.__doc__}")
|
||||
def get_image_album_all(album):
|
||||
with thirdpartylock:
|
||||
for service in services["metadata"]:
|
||||
try:
|
||||
res = service.get_image_album(album)
|
||||
if res:
|
||||
log("Got album image for " + str(album) + " from " + service.name)
|
||||
return res
|
||||
else:
|
||||
log(f"Could not get album image for {album} from {service.name}")
|
||||
except Exception as e:
|
||||
log(f"Error getting album image from {service.name}: {e.__doc__}")
|
||||
|
||||
|
||||
class GenericInterface:
|
||||
@ -177,6 +197,8 @@ class MetadataInterface(GenericInterface,abstract=True):
|
||||
"activated_setting":None
|
||||
}
|
||||
|
||||
delay = 0
|
||||
|
||||
# service provides this role only if the setting is active AND all
|
||||
# necessary auth settings exist
|
||||
def active_metadata(self):
|
||||
@ -200,6 +222,7 @@ class MetadataInterface(GenericInterface,abstract=True):
|
||||
else:
|
||||
imgurl = None
|
||||
if imgurl is not None: imgurl = self.postprocess_url(imgurl)
|
||||
time.sleep(self.delay)
|
||||
return imgurl
|
||||
|
||||
def get_image_artist(self,artist):
|
||||
@ -215,6 +238,25 @@ class MetadataInterface(GenericInterface,abstract=True):
|
||||
else:
|
||||
imgurl = None
|
||||
if imgurl is not None: imgurl = self.postprocess_url(imgurl)
|
||||
time.sleep(self.delay)
|
||||
return imgurl
|
||||
|
||||
def get_image_album(self,album):
|
||||
artists, title = album
|
||||
artiststring = urllib.parse.quote(", ".join(artists or []))
|
||||
titlestring = urllib.parse.quote(title)
|
||||
response = urllib.request.urlopen(
|
||||
self.metadata["albumurl"].format(artist=artiststring,title=titlestring,**self.settings)
|
||||
)
|
||||
|
||||
responsedata = response.read()
|
||||
if self.metadata["response_type"] == "json":
|
||||
data = json.loads(responsedata)
|
||||
imgurl = self.metadata_parse_response_album(data)
|
||||
else:
|
||||
imgurl = None
|
||||
if imgurl is not None: imgurl = self.postprocess_url(imgurl)
|
||||
time.sleep(self.delay)
|
||||
return imgurl
|
||||
|
||||
# default function to parse response by descending down nodes
|
||||
@ -225,19 +267,30 @@ class MetadataInterface(GenericInterface,abstract=True):
|
||||
def metadata_parse_response_track(self,data):
|
||||
return self._parse_response("response_parse_tree_track", data)
|
||||
|
||||
def metadata_parse_response_album(self,data):
|
||||
return self._parse_response("response_parse_tree_album", data)
|
||||
|
||||
def _parse_response(self, resp, data):
|
||||
res = data
|
||||
for node in self.metadata[resp]:
|
||||
try:
|
||||
res = res[node]
|
||||
except Exception:
|
||||
return None
|
||||
handleresult = self.handle_json_result_error(data) #allow the handler to throw custom exceptions
|
||||
# it can also return True to indicate that this is not an error, but simply an instance of 'this api doesnt have any info'
|
||||
if handleresult is True:
|
||||
return None
|
||||
#throw the generic error if the handler refused to do anything
|
||||
raise InvalidResponse()
|
||||
return res
|
||||
|
||||
def postprocess_url(self,url):
|
||||
url = url.replace("http:","https:",1)
|
||||
return url
|
||||
|
||||
def handle_json_result_error(self,result):
|
||||
raise InvalidResponse()
|
||||
|
||||
|
||||
|
||||
|
||||
|
8
maloja/thirdparty/audiodb.py
vendored
8
maloja/thirdparty/audiodb.py
vendored
@ -9,13 +9,17 @@ class AudioDB(MetadataInterface):
|
||||
}
|
||||
|
||||
metadata = {
|
||||
#"trackurl": "https://theaudiodb.com/api/v1/json/{api_key}/searchtrack.php?s={artist}&t={title}",
|
||||
#"trackurl": "https://theaudiodb.com/api/v1/json/{api_key}/searchtrack.php?s={artist}&t={title}", #patreon
|
||||
"artisturl": "https://www.theaudiodb.com/api/v1/json/{api_key}/search.php?s={artist}",
|
||||
#"albumurl": "https://www.theaudiodb.com/api/v1/json/{api_key}/searchalbum.php?s={artist}&a={title}", #patreon
|
||||
"response_type":"json",
|
||||
#"response_parse_tree_track": ["tracks",0,"astrArtistThumb"],
|
||||
"response_parse_tree_artist": ["artists",0,"strArtistThumb"],
|
||||
"required_settings": ["api_key"],
|
||||
}
|
||||
|
||||
def get_image_track(self,artist):
|
||||
def get_image_track(self,track):
|
||||
return None
|
||||
|
||||
def get_image_album(self,album):
|
||||
return None
|
||||
|
28
maloja/thirdparty/deezer.py
vendored
28
maloja/thirdparty/deezer.py
vendored
@ -1,4 +1,5 @@
|
||||
from . import MetadataInterface
|
||||
from . import MetadataInterface, RateLimitExceeded
|
||||
|
||||
|
||||
class Deezer(MetadataInterface):
|
||||
name = "Deezer"
|
||||
@ -8,10 +9,31 @@ class Deezer(MetadataInterface):
|
||||
}
|
||||
|
||||
metadata = {
|
||||
"trackurl": "https://api.deezer.com/search?q={artist}%20{title}",
|
||||
#"trackurl": "https://api.deezer.com/search?q={artist}%20{title}",
|
||||
"artisturl": "https://api.deezer.com/search?q={artist}",
|
||||
"albumurl": "https://api.deezer.com/search?q={artist}%20{title}",
|
||||
"response_type":"json",
|
||||
"response_parse_tree_track": ["data",0,"album","cover_medium"],
|
||||
#"response_parse_tree_track": ["data",0,"album","cover_medium"],
|
||||
"response_parse_tree_artist": ["data",0,"artist","picture_medium"],
|
||||
"response_parse_tree_album": ["data",0,"album","cover_medium"],
|
||||
"required_settings": [],
|
||||
}
|
||||
|
||||
delay = 1
|
||||
|
||||
def get_image_track(self,track):
|
||||
return None
|
||||
# we can use the album pic from the track search,
|
||||
# but should do so via maloja logic
|
||||
|
||||
|
||||
def handle_json_result_error(self,result):
|
||||
if result.get('data') == []:
|
||||
return True
|
||||
if result.get('error',{}).get('code',None) == 4:
|
||||
self.delay += 1
|
||||
# this is permanent (for the lifetime of the process)
|
||||
# but that's actually ok
|
||||
# since hitting the rate limit means we are doing this too fast
|
||||
# and these requests arent really time sensitive
|
||||
raise RateLimitExceeded()
|
||||
|
9
maloja/thirdparty/lastfm.py
vendored
9
maloja/thirdparty/lastfm.py
vendored
@ -22,15 +22,22 @@ class LastFM(MetadataInterface, ProxyScrobbleInterface):
|
||||
"activated_setting": "SCROBBLE_LASTFM"
|
||||
}
|
||||
metadata = {
|
||||
#"artisturl": "https://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artist}&api_key={apikey}&format=json"
|
||||
"trackurl": "https://ws.audioscrobbler.com/2.0/?method=track.getinfo&track={title}&artist={artist}&api_key={apikey}&format=json",
|
||||
"albumurl": "https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={apikey}&artist={artist}&album={title}&format=json",
|
||||
"response_type":"json",
|
||||
"response_parse_tree_track": ["track","album","image",-1,"#text"],
|
||||
# technically just the album artwork, but we use it for now
|
||||
#"response_parse_tree_artist": ["artist","image",-1,"#text"],
|
||||
"response_parse_tree_album": ["album","image",-1,"#text"],
|
||||
"required_settings": ["apikey"],
|
||||
}
|
||||
|
||||
def get_image_artist(self,artist):
|
||||
return None
|
||||
# lastfm doesn't provide artist images
|
||||
# lastfm still provides that endpoint with data,
|
||||
# but doesn't provide actual images
|
||||
|
||||
|
||||
def proxyscrobble_parse_response(self,data):
|
||||
return data.attrib.get("status") == "ok" and data.find("scrobbles").attrib.get("ignored") == "0"
|
||||
|
2
maloja/thirdparty/musicbrainz.py
vendored
2
maloja/thirdparty/musicbrainz.py
vendored
@ -26,6 +26,8 @@ class MusicBrainz(MetadataInterface):
|
||||
return None
|
||||
# not supported
|
||||
|
||||
def get_image_album(self,album):
|
||||
return None
|
||||
|
||||
def get_image_track(self,track):
|
||||
self.lock.acquire()
|
||||
|
10
maloja/thirdparty/spotify.py
vendored
10
maloja/thirdparty/spotify.py
vendored
@ -14,10 +14,12 @@ class Spotify(MetadataInterface):
|
||||
}
|
||||
|
||||
metadata = {
|
||||
"trackurl": "https://api.spotify.com/v1/search?q=artist:{artist}%20track:{title}&type=track&access_token={token}",
|
||||
"artisturl": "https://api.spotify.com/v1/search?q=artist:{artist}&type=artist&access_token={token}",
|
||||
"trackurl": "https://api.spotify.com/v1/search?q={title}%20artist:{artist}&type=track&access_token={token}",
|
||||
"albumurl": "https://api.spotify.com/v1/search?q={title}%20artist:{artist}&type=album&access_token={token}",
|
||||
"artisturl": "https://api.spotify.com/v1/search?q={artist}&type=artist&access_token={token}",
|
||||
"response_type":"json",
|
||||
"response_parse_tree_track": ["tracks","items",0,"album","images",0,"url"],
|
||||
"response_parse_tree_track": ["tracks","items",0,"album","images",0,"url"], # use album art
|
||||
"response_parse_tree_album": ["albums","items",0,"images",0,"url"],
|
||||
"response_parse_tree_artist": ["artists","items",0,"images",0,"url"],
|
||||
"required_settings": ["apiid","secret"],
|
||||
}
|
||||
@ -44,7 +46,7 @@ class Spotify(MetadataInterface):
|
||||
else:
|
||||
expire = responsedata.get("expires_in",3600)
|
||||
self.settings["token"] = responsedata["access_token"]
|
||||
log("Successfully authenticated with Spotify")
|
||||
#log("Successfully authenticated with Spotify")
|
||||
Timer(expire,self.authorize).start()
|
||||
except Exception as e:
|
||||
log("Error while authenticating with Spotify: " + repr(e))
|
||||
|
@ -11,6 +11,9 @@ from .pkg_global.conf import data_dir, dir_settings
|
||||
from .apis import _apikeys
|
||||
|
||||
|
||||
from .database.sqldb import get_maloja_info, set_maloja_info
|
||||
|
||||
|
||||
# Dealing with old style tsv files - these should be phased out everywhere
|
||||
def read_tsvs(path,types):
|
||||
result = []
|
||||
@ -40,7 +43,7 @@ def upgrade_apikeys():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# v2 to v3 iupgrade
|
||||
def upgrade_db(callback_add_scrobbles):
|
||||
|
||||
oldfolder = os.path.join(dir_settings['state'],"scrobbles")
|
||||
@ -88,3 +91,13 @@ def upgrade_db(callback_add_scrobbles):
|
||||
callback_add_scrobbles(scrobblelist)
|
||||
os.rename(os.path.join(oldfolder,sf),os.path.join(newfolder,sf))
|
||||
log("Done!",color='yellow')
|
||||
|
||||
|
||||
# 3.2 album support
|
||||
def parse_old_albums():
|
||||
setting_name = "db_upgrade_albums"
|
||||
if get_maloja_info([setting_name]).get(setting_name):
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
#set_maloja_info({setting_name:True})
|
||||
|
@ -21,7 +21,8 @@
|
||||
['setup','Server Setup'],
|
||||
['settings','Settings'],
|
||||
['apikeys','API Keys'],
|
||||
['manual','Manual Scrobbling']
|
||||
['manual','Manual Scrobbling'],
|
||||
['albumless','Tracks without Albums']
|
||||
|
||||
] %}
|
||||
{# ['import','Scrobble Import'],
|
||||
|
@ -23,13 +23,24 @@
|
||||
<script src="/neopolitan.js"></script>
|
||||
<script src="/upload.js"></script>
|
||||
<script src="/notifications.js"></script>
|
||||
<script src="/edit.js"></script>
|
||||
<script>
|
||||
const defaultpicks = {
|
||||
topartists: '{{ settings["DEFAULT_RANGE_STARTPAGE"] }}',
|
||||
toptracks: '{{ settings["DEFAULT_RANGE_STARTPAGE"] }}',
|
||||
topalbums: '{{ settings["DEFAULT_RANGE_STARTPAGE"] }}',
|
||||
pulse: '{{ settings["DEFAULT_RANGE_STARTPAGE"] }}',
|
||||
pulseperformancecombined: '{{ settings["DEFAULT_RANGE_STARTPAGE"] }}',
|
||||
featured: 'artist'
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preload" href="/static/ttf/Ubuntu-Regular.ttf" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="{% block custombodyclasses %}{% endblock %}">
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -56,22 +67,17 @@
|
||||
|
||||
|
||||
|
||||
<div id="footer">
|
||||
<div id="left-side">
|
||||
<a href="/about">About</a>
|
||||
</div>
|
||||
<div id="notch">
|
||||
<a href="/"><img style="display:block;" src="/favicon.png" /></a>
|
||||
</div>
|
||||
<div id="right-side">
|
||||
<span><input id="searchinput" placeholder="Search for an artist or track..." oninput="search(this)" onblur="clearresults()" /></span>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div>
|
||||
<!--<span>Get your own charts on
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://github.com/krateng/maloja">GitHub</a>,
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://pypi.org/project/malojaserver/">PyPI</a> or
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://hub.docker.com/r/krateng/maloja">Dockerhub</a>
|
||||
</span>-->
|
||||
<span><a href="/about">About</a></span>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/"><span style="font-weight:bold;">Maloja {% if settings["DEV_MODE"] %}[Developer Mode]{% endif %}</span></a>
|
||||
</div>
|
||||
<div>
|
||||
<span><input id="searchinput" placeholder="Search for an artist or track..." oninput="search(this)" onblur="clearresults()" /></span>
|
||||
</div>
|
||||
|
||||
<div id="resultwrap" class="hide">
|
||||
<div class="searchresults">
|
||||
@ -80,7 +86,11 @@
|
||||
</table>
|
||||
<br/><br/>
|
||||
<span>Tracks</span>
|
||||
<table class="searchresults_tracks" id="searchresults_tracks">
|
||||
<table class="searchresults_tracks searchresults_extrainfo" id="searchresults_tracks">
|
||||
</table>
|
||||
<br/><br/>
|
||||
<span>Albums</span>
|
||||
<table class="searchresults_albums searchresults_extrainfo" id="searchresults_albums">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
12
maloja/web/jinja/admin_albumless.jinja
Normal file
12
maloja/web/jinja/admin_albumless.jinja
Normal file
@ -0,0 +1,12 @@
|
||||
{% set page ='admin_albumless' %}
|
||||
{% extends "abstracts/admin.jinja" %}
|
||||
{% block title %}Maloja - Albumless Tracks{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
Here you can find tracks that currently have no album.<br/><br/>
|
||||
|
||||
{% with list = dbc.get_tracks_without_album() %}
|
||||
{% include 'partials/list_tracks.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
@ -4,6 +4,14 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/manualscrobble.js"></script>
|
||||
<style>
|
||||
.tooltip {
|
||||
cursor: help;
|
||||
}
|
||||
.tooltip:hover {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -26,14 +34,49 @@
|
||||
<input placeholder='Enter to scrobble' class='simpleinput' id='title' onKeydown='scrobbleIfEnter(event)' />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-right:7px;">
|
||||
Album artists (Optional):
|
||||
</td><td id="albumartists_td">
|
||||
<input placeholder='Separate with Enter' class='simpleinput' id='albumartists' onKeydown='keyDetect2(event)' onblur='addEnteredAlbumartist()' />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-right:7px;">
|
||||
Album (Optional):
|
||||
</td><td>
|
||||
<input placeholder='Enter to scrobble' class='simpleinput' id='album' onKeydown='scrobbleIfEnter(event)' />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" id="use_custom_time" />
|
||||
Custom Time:
|
||||
</td>
|
||||
<td>
|
||||
<input id="scrobble_datetime" type="datetime-local">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<script>
|
||||
const now = new Date();
|
||||
const localDateTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||||
document.getElementById("scrobble_datetime").value = localDateTime;
|
||||
</script>
|
||||
|
||||
<br/>
|
||||
|
||||
<input type="checkbox" id="use_track_artists_for_album" checked='true' />
|
||||
<span class="tooltip" title="If this is unchecked, specifying no album artists will result in a compilation album ('Various Artists')">Use track artists as album artists fallback</span>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<button type="button" onclick="scrobbleNew(event)">Scrobble!</button>
|
||||
<button type="button" onclick="repeatLast()">↻</button>
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
|
||||
|
144
maloja/web/jinja/album.jinja
Normal file
144
maloja/web/jinja/album.jinja
Normal file
@ -0,0 +1,144 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - {{ info.album.albumtitle }}{% endblock %}
|
||||
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/statselect.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% set album = filterkeys.album %}
|
||||
{% set info = dbc.album_info({'album':album}) %}
|
||||
|
||||
{% set initialrange ='month' %}
|
||||
|
||||
|
||||
{% set encodedalbum = mlj_uri.uriencode({'album':album}) %}
|
||||
|
||||
{% block custombodyclasses %}
|
||||
{% if info.certification %}certified certified_{{ info.certification }}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block icon_bar %}
|
||||
{% if adminmode %}
|
||||
{% include 'icons/edit.jinja' %}
|
||||
|
||||
<div class="iconsubset mergeicons" data-entity_type="album" data-entity_id="{{ info.id }}" data-entity_name="{{ info.album.albumtitle }}">
|
||||
{% include 'icons/merge.jinja' %}
|
||||
{% include 'icons/merge_mark.jinja' %}
|
||||
{% include 'icons/merge_unmark.jinja' %}
|
||||
{% include 'icons/merge_cancel.jinja' %}
|
||||
</div>
|
||||
|
||||
<div class="iconsubset associateicons" data-entity_type="album" data-entity_id="{{ info.id }}" data-entity_name="{{ info.album.albumtitle }}">
|
||||
{% include 'icons/add_album.jinja' %}
|
||||
<!-- no remove album since that is not a specified association - every track only has one album, so the removal should
|
||||
be handled on the track page (or for now, not at all) -->
|
||||
{% include 'icons/association_mark.jinja' %}
|
||||
{% include 'icons/association_unmark.jinja' %}
|
||||
{% include 'icons/association_cancel.jinja' %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
const entity_id = {{ info.id }};
|
||||
const entity_type = 'album';
|
||||
const entity_name = {{ album.albumtitle | tojson }};
|
||||
</script>
|
||||
|
||||
|
||||
{% import 'partials/awards_album.jinja' as awards %}
|
||||
|
||||
|
||||
{% include 'partials/info_album.jinja' %}
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_tracks",filterkeys) }}'>Top Tracks</a></h2>
|
||||
|
||||
|
||||
{% with amountkeys={"perpage":16,"page":0} %}
|
||||
{% include 'partials/charts_tracks.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
<table class="twopart">
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<h2 class="headerwithextra"><a href='{{ mlj_uri.create_uri("/pulse",filterkeys) }}'>Pulse</a></h2>
|
||||
<br/>
|
||||
{% for r in xranges %}
|
||||
<span
|
||||
onclick="showStatsManual('pulseperformancecombined','{{ r.identifier }}')"
|
||||
class="stat_selector_pulseperformancecombined selector_pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'opacity:0.5;' if initialrange==r.identifier else '' }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{% if not loop.last %}|{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<br/><br/>
|
||||
|
||||
{% for r in xranges %}
|
||||
|
||||
<span
|
||||
class="stat_module_pulseperformancecombined pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'display:none;' if initialrange!=r.identifier else '' }}"
|
||||
>
|
||||
|
||||
{% with limitkeys={"since":r.firstrange},delimitkeys={'step':r.identifier,'trail':1} %}
|
||||
{% include 'partials/pulse.jinja' %}
|
||||
{% endwith %}
|
||||
</span>
|
||||
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<!-- We use the same classes / function calls here because we want it to switch together with pulse -->
|
||||
<h2 class="headerwithextra"><a href='{{ mlj_uri.create_uri("/performance",filterkeys) }}'>Performance</a></h2>
|
||||
<br/>
|
||||
{% for r in xranges %}
|
||||
<span
|
||||
onclick="showStatsManual('pulseperformancecombined','{{ r.identifier }}')"
|
||||
class="stat_selector_pulseperformancecombined selector_pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'opacity:0.5;' if initialrange==r.identifier else '' }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{% if not loop.last %}|{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<br/><br/>
|
||||
|
||||
{% for r in xranges %}
|
||||
|
||||
<span
|
||||
class="stat_module_pulseperformancecombined pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'display:none;' if initialrange!=r.identifier else '' }}"
|
||||
>
|
||||
|
||||
{% with limitkeys={"since":r.firstrange},delimitkeys={'step':r.identifier,'trail':1} %}
|
||||
{% include 'partials/performance.jinja' %}
|
||||
{% endwith %}
|
||||
</span>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}'>Last Scrobbles</a></h2>
|
||||
|
||||
{% with amountkeys = {"perpage":16,"page":0} %}
|
||||
{% include 'partials/scrobbles.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
{% endblock %}
|
@ -5,12 +5,11 @@
|
||||
{% import 'partials/awards_artist.jinja' as awards %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/rangeselect.js"></script>
|
||||
<script src="/edit.js"></script>
|
||||
<script src="/statselect.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% set artist = filterkeys.artist %}
|
||||
{% set info = db.artist_info(artist=artist) %}
|
||||
{% set info = dbc.artist_info({'artist':artist}) %}
|
||||
|
||||
{% set credited = info.get('replace') %}
|
||||
{% set included = info.get('associated') %}
|
||||
@ -27,13 +26,27 @@
|
||||
|
||||
{% set encodedartist = mlj_uri.uriencode({'artist':artist}) %}
|
||||
|
||||
{% block custombodyclasses %}
|
||||
{% if info.certification %}certified certified_{{ info.certification }}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block icon_bar %}
|
||||
{% if adminmode %}
|
||||
{% include 'icons/edit.jinja' %}
|
||||
|
||||
<div class="iconsubset mergeicons" data-entity_type="artist" data-entity_id="{{ info.id }}" data-entity_name="{{ info.artist }}">
|
||||
{% include 'icons/merge.jinja' %}
|
||||
{% include 'icons/merge_mark.jinja' %}
|
||||
{% include 'icons/merge_unmark.jinja' %}
|
||||
{% include 'icons/merge_cancel.jinja' %}
|
||||
<script>showValidMergeIcons();</script>
|
||||
</div>
|
||||
|
||||
<div class="iconsubset associateicons" data-entity_type="artist" data-entity_id="{{ info.id }}" data-entity_name="{{ info.artist }}">
|
||||
{% include 'icons/add_artist.jinja' %}
|
||||
{% include 'icons/remove_artist.jinja' %}
|
||||
{% include 'icons/association_cancel.jinja' %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -47,56 +60,36 @@
|
||||
|
||||
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
{% if adminmode %}
|
||||
<div
|
||||
class="changeable-image" data-uploader="b64=>upload('{{ encodedartist }}',b64)"
|
||||
style="background-image:url('{{ images.get_artist_image(artist) }}');"
|
||||
title="Drag & Drop to upload new image"
|
||||
></div>
|
||||
{% else %}
|
||||
<div style="background-image:url('{{ images.get_artist_image(artist) }}');">
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1 id="main_entity_name" class="headerwithextra">{{ info.artist | e }}</h1>
|
||||
{% if competes %}<span class="rank"><a href="/charts_artists?max=100">#{{ info.position }}</a></span>{% endif %}
|
||||
<br/>
|
||||
{% if competes and included %}
|
||||
<span>associated: {{ links.links(included) }}</span>
|
||||
{% elif not competes %}
|
||||
<span>Competing under {{ links.link(credited) }} (#{{ info.position }})</span>
|
||||
{% endif %}
|
||||
|
||||
<p class="stats">
|
||||
<a href="{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}">{{ info['scrobbles'] }} Scrobbles</a>
|
||||
</p>
|
||||
{% include 'partials/info_artist.jinja' %}
|
||||
|
||||
|
||||
{% set albums_info = dbc.get_albums_artist_appears_on(filterkeys,limitkeys) %}
|
||||
{% set ownalbums = albums_info.own_albums %}
|
||||
{% set otheralbums = albums_info.appears_on %}
|
||||
|
||||
{% if ownalbums or otheralbums %}
|
||||
|
||||
{% if competes %}
|
||||
{{ awards.medals(info) }}
|
||||
{{ awards.topweeks(info) }}
|
||||
{% endif %}
|
||||
{{ awards.certs(artist) }}
|
||||
{% if settings['ALBUM_SHOWCASE'] %}
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_albums",filterkeys) }}'>Albums</a></h2>
|
||||
{% include 'partials/album_showcase.jinja' %}
|
||||
{% else %}
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_albums",filterkeys) }}'>Top Albums</a></h2>
|
||||
|
||||
{% with amountkeys={"perpage":16,"page":0} %}
|
||||
{% include 'partials/charts_albums.jinja' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if info['scrobbles']>0 %}
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_tracks",filterkeys) }}'>Top Tracks</a></h2>
|
||||
|
||||
|
||||
{% with amountkeys={"perpage":15,"page":0} %}
|
||||
{% with amountkeys={"perpage":16,"page":0} %}
|
||||
{% include 'partials/charts_tracks.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
<table class="twopart">
|
||||
@ -107,8 +100,8 @@
|
||||
<br/>
|
||||
{% for r in xranges %}
|
||||
<span
|
||||
onclick="showRangeManual('pulse','{{ r.identifier }}')"
|
||||
class="stat_selector_pulse selector_pulse_{{ r.identifier }}"
|
||||
onclick="showStatsManual('pulseperformancecombined','{{ r.identifier }}')"
|
||||
class="stat_selector_pulseperformancecombined selector_pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'opacity:0.5;' if initialrange==r.identifier else '' }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
@ -120,7 +113,7 @@
|
||||
{% for r in xranges %}
|
||||
|
||||
<span
|
||||
class="stat_module_pulse pulse_{{ r.identifier }}"
|
||||
class="stat_module_pulseperformancecombined pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'display:none;' if initialrange!=r.identifier else '' }}"
|
||||
>
|
||||
|
||||
@ -140,8 +133,8 @@
|
||||
|
||||
{% for r in xranges %}
|
||||
<span
|
||||
onclick="showRangeManual('pulse','{{ r.identifier }}')"
|
||||
class="stat_selector_pulse selector_pulse_{{ r.identifier }}"
|
||||
onclick="showStatsManual('pulseperformancecombined','{{ r.identifier }}')"
|
||||
class="stat_selector_pulseperformancecombined selector_pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'opacity:0.5;' if initialrange==r.identifier else '' }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
@ -153,7 +146,7 @@
|
||||
{% for r in xranges %}
|
||||
|
||||
<span
|
||||
class="stat_module_pulse pulse_{{ r.identifier }}"
|
||||
class="stat_module_pulseperformancecombined pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'display:none;' if initialrange!=r.identifier else '' }}"
|
||||
>
|
||||
|
||||
@ -171,8 +164,9 @@
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}'>Last Scrobbles</a></h2>
|
||||
|
||||
{% with amountkeys = {"perpage":15,"page":0} %}
|
||||
{% with amountkeys = {"perpage":16,"page":0} %}
|
||||
{% include 'partials/scrobbles.jinja' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
52
maloja/web/jinja/charts_albums.jinja
Normal file
52
maloja/web/jinja/charts_albums.jinja
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - Album Charts{% endblock %}
|
||||
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/datechange.js" async></script>
|
||||
{% endblock %}
|
||||
|
||||
{% set charts = dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':False}) %}
|
||||
{% set pages = math.ceil(charts.__len__() / amountkeys.perpage) %}
|
||||
{% if charts[0] is defined %}
|
||||
{% set topalbum = charts[0].album %}
|
||||
{% set img = images.get_album_image(topalbum) %}
|
||||
{% else %}
|
||||
{% set img = "/favicon.png" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('{{ img }}')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Album Charts</h1><a href="/top_albums"><span>View #1 Albums</span></a><br/>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
<br/><br/>
|
||||
{% with delimitkeys = {} %}
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if settings['CHARTS_DISPLAY_TILES'] %}
|
||||
{% include 'partials/charts_albums_tiles.jinja' %}
|
||||
<br/><br/>
|
||||
{% endif %}
|
||||
|
||||
{% with compare=true %}
|
||||
{% include 'partials/charts_albums.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
{% import 'snippets/pagination.jinja' as pagination %}
|
||||
{{ pagination.pagination(filterkeys,limitkeys,delimitkeys,amountkeys,pages) }}
|
||||
|
||||
{% endblock %}
|
@ -1,11 +1,16 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - Artist Charts{% endblock %}
|
||||
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/datechange.js" async></script>
|
||||
{% endblock %}
|
||||
|
||||
{% set charts = dbc.get_charts_artists(filterkeys,limitkeys) %}
|
||||
{% set charts = dbc.get_charts_artists(filterkeys,limitkeys,specialkeys) %}
|
||||
|
||||
|
||||
|
||||
{% set pages = math.ceil(charts.__len__() / amountkeys.perpage) %}
|
||||
{% if charts[0] is defined %}
|
||||
{% set topartist = charts[0].artist %}
|
||||
@ -25,9 +30,9 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Artist Charts</h1><a href="/top_artists"><span>View #1 Artists</span></a><br/>
|
||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
<br/><br/>
|
||||
{% with delimitkeys = {} %}
|
||||
{% with delimitkeys = {}, artistchart=True %}
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% block title %}Maloja - Track Charts{% endblock %}
|
||||
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/datechange.js" async></script>
|
||||
@ -26,8 +27,7 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Track Charts</h1><a href="/top_tracks"><span>View #1 Tracks</span></a><br/>
|
||||
{% if filterkeys.get('artist') is not none %}by {{ links.link(filterkeys.get('artist')) }}{% endif %}
|
||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
<br/><br/>
|
||||
{% with delimitkeys = {} %}
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
|
6
maloja/web/jinja/icons/add_album.jinja
Normal file
6
maloja/web/jinja/icons/add_album.jinja
Normal file
@ -0,0 +1,6 @@
|
||||
<div title="Add to Album" id="associatealbumicon" class="clickable_icon" onclick="associate(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M2 4.75C2 3.784 2.784 3 3.75 3h4.971a1.75 1.75 0 0 1 1.447.765l1.404 2.063a.25.25 0 0 0 .207.11h8.471c.966 0 1.75.783 1.75 1.75V19.25A1.75 1.75 0 0 1 20.25 21H4.75a.75.75 0 0 1 0-1.5h15.5a.25.25 0 0 0 .25-.25V7.688a.25.25 0 0 0-.25-.25h-8.471a1.751 1.751 0 0 1-1.447-.766L8.928 4.609a.252.252 0 0 0-.207-.109H3.75a.25.25 0 0 0-.25.25v3.5a.75.75 0 0 1-1.5 0v-3.5Z"></path>
|
||||
<path d="m9.308 12.5-2.104-2.236a.75.75 0 1 1 1.092-1.028l3.294 3.5a.75.75 0 0 1 0 1.028l-3.294 3.5a.75.75 0 1 1-1.092-1.028L9.308 14H4.09a2.59 2.59 0 0 0-2.59 2.59v3.16a.75.75 0 0 1-1.5 0v-3.16a4.09 4.09 0 0 1 4.09-4.09h5.218Z"></path>
|
||||
</svg>
|
||||
</div>
|
5
maloja/web/jinja/icons/add_album_confirm.jinja
Normal file
5
maloja/web/jinja/icons/add_album_confirm.jinja
Normal file
@ -0,0 +1,5 @@
|
||||
<div title="Add Track to this Album" id="addalbumconfirmicon" class="clickable_icon" onclick="addAlbum()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M2 4.75C2 3.784 2.784 3 3.75 3h4.971c.58 0 1.12.286 1.447.765l1.404 2.063c.046.069.124.11.207.11h8.471c.966 0 1.75.783 1.75 1.75V19.25A1.75 1.75 0 0 1 20.25 21H3.75A1.75 1.75 0 0 1 2 19.25Z"></path>
|
||||
</svg>
|
||||
</div>
|
5
maloja/web/jinja/icons/add_artist.jinja
Normal file
5
maloja/web/jinja/icons/add_artist.jinja
Normal file
@ -0,0 +1,5 @@
|
||||
<div title="Add Artist" id="associateartisticon" class="clickable_icon" onclick="associate(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M4 9.5a5 5 0 1 1 7.916 4.062 7.973 7.973 0 0 1 5.018 7.166.75.75 0 1 1-1.499.044 6.469 6.469 0 0 0-12.932 0 .75.75 0 0 1-1.499-.044 7.972 7.972 0 0 1 5.059-7.181A4.994 4.994 0 0 1 4 9.5ZM9 6a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Zm10.25-5a.75.75 0 0 1 .75.75V4h2.25a.75.75 0 0 1 0 1.5H20v2.25a.75.75 0 0 1-1.5 0V5.5h-2.25a.75.75 0 0 1 0-1.5h2.25V1.75a.75.75 0 0 1 .75-.75Z"></path>
|
||||
</svg>
|
||||
</div>
|
5
maloja/web/jinja/icons/add_artist_confirm.jinja
Normal file
5
maloja/web/jinja/icons/add_artist_confirm.jinja
Normal file
@ -0,0 +1,5 @@
|
||||
<div title="Add this Artist to Track" id="addartistconfirmicon" class="clickable_icon" onclick="addArtist()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M12 2.5a5.25 5.25 0 0 0-2.519 9.857 9.005 9.005 0 0 0-6.477 8.37.75.75 0 0 0 .727.773H20.27a.75.75 0 0 0 .727-.772 9.005 9.005 0 0 0-6.477-8.37A5.25 5.25 0 0 0 12 2.5Z"></path>
|
||||
</svg>
|
||||
</div>
|
6
maloja/web/jinja/icons/association_cancel.jinja
Normal file
6
maloja/web/jinja/icons/association_cancel.jinja
Normal file
@ -0,0 +1,6 @@
|
||||
<div title="Cancel associating" class="associatecancelicon clickable_icon" onclick="cancelAssociate(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M12 1c6.075 0 11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1ZM5.834 19.227A9.464 9.464 0 0 0 12 21.5a9.5 9.5 0 0 0 9.5-9.5 9.464 9.464 0 0 0-2.273-6.166ZM2.5 12a9.464 9.464 0 0 0 2.273 6.166L18.166 4.773A9.463 9.463 0 0 0 12 2.5 9.5 9.5 0 0 0 2.5 12Z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
5
maloja/web/jinja/icons/association_mark.jinja
Normal file
5
maloja/web/jinja/icons/association_mark.jinja
Normal file
@ -0,0 +1,5 @@
|
||||
<div title="Mark for association" class="associationmarkicon clickable_icon" onclick="markForAssociate(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="m16.114 1.553 6.333 6.333a1.75 1.75 0 0 1-.603 2.869l-1.63.633a5.67 5.67 0 0 0-3.395 3.725l-1.131 3.959a1.75 1.75 0 0 1-2.92.757L9 16.061l-5.595 5.594a.749.749 0 1 1-1.06-1.06L7.939 15l-3.768-3.768a1.75 1.75 0 0 1 .757-2.92l3.959-1.131a5.666 5.666 0 0 0 3.725-3.395l.633-1.63a1.75 1.75 0 0 1 2.869-.603ZM5.232 10.171l8.597 8.597a.25.25 0 0 0 .417-.108l1.131-3.959A7.17 7.17 0 0 1 19.67 9.99l1.63-.634a.25.25 0 0 0 .086-.409l-6.333-6.333a.25.25 0 0 0-.409.086l-.634 1.63a7.17 7.17 0 0 1-4.711 4.293L5.34 9.754a.25.25 0 0 0-.108.417Z"></path>
|
||||
</svg>
|
||||
</div>
|
7
maloja/web/jinja/icons/association_unmark.jinja
Normal file
7
maloja/web/jinja/icons/association_unmark.jinja
Normal file
@ -0,0 +1,7 @@
|
||||
<div title="Unmark for Association"class="associationunmarkicon clickable_icon" onclick="umarkForAssociate(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M2.345 20.595 8.47 14.47q.219-.22.53-.22.311 0 .53.22.22.219.22.53 0 .311-.22.53l-6.125 6.125q-.219.22-.53.22-.311 0-.53-.22-.22-.219-.22-.53 0-.311.22-.53Z"></path>
|
||||
<path d="m16.72 11.97.358-.358a6.738 6.738 0 0 1 2.326-1.518l1.896-.738a.25.25 0 0 0 .086-.409l-6.333-6.333a.25.25 0 0 0-.409.086l-.521 1.34a8.663 8.663 0 0 1-2.243 3.265.75.75 0 0 1-1.01-1.11 7.132 7.132 0 0 0 1.854-2.699l.521-1.34a1.75 1.75 0 0 1 2.869-.603l6.333 6.333a1.75 1.75 0 0 1-.603 2.869l-1.896.737a5.26 5.26 0 0 0-1.81 1.18l-.358.358a.749.749 0 1 1-1.06-1.06Zm-12.549-.738a1.75 1.75 0 0 1 .757-2.92l3.366-.962.412 1.443-3.366.961a.25.25 0 0 0-.108.417l8.597 8.597a.25.25 0 0 0 .417-.108l.961-3.366 1.443.412-.962 3.366a1.75 1.75 0 0 1-2.92.757Z"></path>
|
||||
<path d="m3.405 2.095 18.75 18.75q.22.219.22.53 0 .311-.22.53-.219.22-.53.22-.311 0-.53-.22L2.345 3.155q-.22-.219-.22-.53 0-.311.22-.53.219-.22.53-.22.311 0 .53.22Z"></path>
|
||||
</svg>
|
||||
</div>
|
24
maloja/web/jinja/icons/cert_album.jinja
Normal file
24
maloja/web/jinja/icons/cert_album.jinja
Normal file
@ -0,0 +1,24 @@
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="15" weight="15" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 511.996 511.996" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M0,42.667v426.667c0,11.776,9.536,21.333,21.333,21.333h64V21.333h-64C9.536,21.333,0,30.891,0,42.667z"/>
|
||||
<path d="M234.667,363.855c0,11.115,9.557,20.139,21.333,20.139c11.563,0,21.333-9.771,21.333-21.333v-21.333H256
|
||||
C244.843,341.327,234.667,352.058,234.667,363.855z"/>
|
||||
<path d="M490.662,21.329H127.996v469.333h362.667c11.797,0,21.333-9.557,21.333-21.333V42.662
|
||||
C511.996,30.886,502.46,21.329,490.662,21.329z M400.188,250.428l-6.379,20.16c-2.88,9.088-11.264,14.912-20.331,14.912
|
||||
c-2.133,0-4.288-0.32-6.443-1.003c-11.221-3.541-17.451-15.531-13.888-26.773l6.485-20.48
|
||||
c6.357-19.157,2.347-40.107-10.432-55.253l-29.205-26.816v164.821v42.667c0,35.307-28.693,64-64,64
|
||||
c-35.285,0-64-28.181-64-62.805c0-35.328,29.312-65.195,64-65.195h21.333v-192c0-1.067,0.469-1.984,0.619-3.008
|
||||
c0.213-1.6,0.341-3.179,0.939-4.693c0.576-1.493,1.536-2.709,2.411-4.011c0.597-0.875,0.896-1.899,1.643-2.709
|
||||
c0.107-0.107,0.256-0.149,0.363-0.256c1.109-1.152,2.496-1.92,3.84-2.795c1.003-0.683,1.899-1.557,2.987-2.027
|
||||
c0.896-0.384,1.92-0.427,2.88-0.683c1.728-0.491,3.456-1.024,5.248-1.067c0.149,0,0.256-0.085,0.405-0.085
|
||||
c1.024,0,1.92,0.448,2.901,0.597c1.643,0.213,3.243,0.363,4.8,0.96c1.536,0.597,2.773,1.579,4.117,2.475
|
||||
c0.853,0.597,1.813,0.875,2.603,1.579l65.899,60.459c0.576,0.512,1.131,1.088,1.643,1.664
|
||||
C403.878,179.644,411.366,216.934,400.188,250.428z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
26
maloja/web/jinja/icons/cert_track.jinja
Normal file
26
maloja/web/jinja/icons/cert_track.jinja
Normal file
@ -0,0 +1,26 @@
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="15" weight="15" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512.002 512.002" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M255.997,0c-34.112,0-66.645,6.848-96.448,19.008l84.053,152.917c4.075-0.597,8.149-1.259,12.395-1.259
|
||||
c47.083,0,85.333,38.293,85.333,85.333c0,5.632-0.597,11.136-1.664,16.469L480.103,379.52
|
||||
c20.288-36.651,31.893-78.741,31.893-123.52C511.997,114.837,397.181,0,255.997,0z"/>
|
||||
<path d="M316.383,316.307c-2.603,2.624-5.376,5.056-8.299,7.275l82.347,149.867c25.173-15.616,47.467-35.349,65.835-58.432
|
||||
L320.714,311.678C319.327,313.257,317.876,314.814,316.383,316.307z"/>
|
||||
<path d="M256.005,341.335c-47.061,0-85.333-38.272-85.333-85.333c0-5.291,0.64-10.432,1.557-15.467L26.565,143.191
|
||||
C9.733,177.282,0.005,215.49,0.005,256.002c0,141.163,114.837,256,256,256c34.155,0,66.688-6.848,96.491-19.029l-84.011-152.896
|
||||
C264.389,340.695,260.272,341.335,256.005,341.335z"/>
|
||||
<path d="M271.602,295.62c2.432-0.939,4.672-2.155,6.848-3.477c0.363-0.235,0.768-0.384,1.131-0.619
|
||||
c1.813-1.195,3.435-2.645,5.035-4.096c0.576-0.533,1.216-0.981,1.771-1.536c3.605-3.648,6.464-8.043,8.747-12.971
|
||||
c2.261-5.184,3.541-10.901,3.541-16.917c0-23.531-19.136-42.667-42.667-42.667c-5.504,0-10.709,1.131-15.552,3.029
|
||||
c-2.645,1.045-5.141,2.347-7.488,3.84c-0.107,0.064-0.256,0.107-0.363,0.192c-2.432,1.579-4.693,3.392-6.72,5.44
|
||||
c-3.947,3.947-7.04,8.555-9.216,13.781c0,0.043-0.043,0.064-0.064,0.107c-2.091,5.013-3.264,10.496-3.264,16.277
|
||||
c0,23.531,19.136,42.667,42.667,42.667C261.533,298.671,266.759,297.54,271.602,295.62z"/>
|
||||
<path d="M195.581,195.834c0.085-0.107,0.192-0.192,0.277-0.277c2.56-2.56,5.269-4.949,8.128-7.147L121.597,38.543
|
||||
c-28.565,17.728-53.376,40.853-73.024,68.032l141.888,94.827C192.082,199.482,193.789,197.626,195.581,195.834z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
5
maloja/web/jinja/icons/disassociate.jinja
Normal file
5
maloja/web/jinja/icons/disassociate.jinja
Normal file
@ -0,0 +1,5 @@
|
||||
<div class='disassociateicon clickable_icon danger' onclick="disassociate(this)" title="Disassociate artists">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20.347 3.653a3.936 3.936 0 0 0-5.567 0l-1.75 1.75a.75.75 0 0 1-1.06-1.06l1.75-1.75a5.436 5.436 0 0 1 7.688 7.687l-1.564 1.564a.75.75 0 0 1-1.06-1.06l1.563-1.564a3.936 3.936 0 0 0 0-5.567ZM9.786 12.369a.75.75 0 0 1 1.053.125c.096.122.2.24.314.353 1.348 1.348 3.386 1.587 4.89.658l-3.922-2.858a.745.745 0 0 1-.057-.037c-1.419-1.013-3.454-.787-4.784.543L3.653 14.78a3.936 3.936 0 0 0 5.567 5.567l3-3a.75.75 0 1 1 1.06 1.06l-3 3a5.436 5.436 0 1 1-7.688-7.687l3.628-3.628a5.517 5.517 0 0 1 3.014-1.547l-7.05-5.136a.75.75 0 0 1 .883-1.213l20.25 14.75a.75.75 0 0 1-.884 1.213l-5.109-3.722c-2.155 1.709-5.278 1.425-7.232-.53a5.491 5.491 0 0 1-.431-.485.75.75 0 0 1 .125-1.053Z"></path>
|
||||
</svg>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
<div title="Merge" id="mergeicon" class="clickable_icon hide" onclick="merge()">
|
||||
<div title="Merge" id="mergeicon" class="clickable_icon" onclick="merge(this)">
|
||||
<svg viewBox="0 0 16 16" width="24" height="24">
|
||||
<path fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>
|
||||
</svg>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<div title="Cancel merge" id="mergecancelicon" class="clickable_icon hide" onclick="cancelMerge()">
|
||||
<svg viewBox="0 0 16 16" width="24" height="24">
|
||||
<path fill-rule="evenodd" d="M10.72 1.227a.75.75 0 011.06 0l.97.97.97-.97a.75.75 0 111.06 1.061l-.97.97.97.97a.75.75 0 01-1.06 1.06l-.97-.97-.97.97a.75.75 0 11-1.06-1.06l.97-.97-.97-.97a.75.75 0 010-1.06zM12.75 6.5a.75.75 0 00-.75.75v3.378a2.251 2.251 0 101.5 0V7.25a.75.75 0 00-.75-.75zm0 5.5a.75.75 0 100 1.5.75.75 0 000-1.5zM2.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.25 1a2.25 2.25 0 00-.75 4.372v5.256a2.251 2.251 0 101.5 0V5.372A2.25 2.25 0 003.25 1zm0 11a.75.75 0 100 1.5.75.75 0 000-1.5z"></path>
|
||||
<div title="Cancel merging" id="mergecancelicon" class="clickable_icon" onclick="cancelMerge(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M12 1c6.075 0 11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1ZM5.834 19.227A9.464 9.464 0 0 0 12 21.5a9.5 9.5 0 0 0 9.5-9.5 9.464 9.464 0 0 0-2.273-6.166ZM2.5 12a9.464 9.464 0 0 0 2.273 6.166L18.166 4.773A9.463 9.463 0 0 0 12 2.5 9.5 9.5 0 0 0 2.5 12Z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div title="Mark for merging" id="mergemarkicon" class="clickable_icon hide" onclick="markForMerge()">
|
||||
<div title="Mark for merging" id="mergemarkicon" class="clickable_icon" onclick="markForMerge(this)">
|
||||
<svg viewBox="0 0 16 16" width="24" height="24">
|
||||
<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path>
|
||||
</svg>
|
||||
|
5
maloja/web/jinja/icons/merge_unmark.jinja
Normal file
5
maloja/web/jinja/icons/merge_unmark.jinja
Normal file
@ -0,0 +1,5 @@
|
||||
<div title="Unmark from merge" id="mergeunmarkicon" class="clickable_icon" onclick="unmarkForMerge(this)">
|
||||
<svg viewBox="0 0 16 16" width="24" height="24">
|
||||
<path fill-rule="evenodd" d="M10.72 1.227a.75.75 0 011.06 0l.97.97.97-.97a.75.75 0 111.06 1.061l-.97.97.97.97a.75.75 0 01-1.06 1.06l-.97-.97-.97.97a.75.75 0 11-1.06-1.06l.97-.97-.97-.97a.75.75 0 010-1.06zM12.75 6.5a.75.75 0 00-.75.75v3.378a2.251 2.251 0 101.5 0V7.25a.75.75 0 00-.75-.75zm0 5.5a.75.75 0 100 1.5.75.75 0 000-1.5zM2.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.25 1a2.25 2.25 0 00-.75 4.372v5.256a2.251 2.251 0 101.5 0V5.372A2.25 2.25 0 003.25 1zm0 11a.75.75 0 100 1.5.75.75 0 000-1.5z"></path>
|
||||
</svg>
|
||||
</div>
|
@ -1,7 +1,7 @@
|
||||
<td style="opacity:0.5;text-align:center;">
|
||||
<div class="tile" style="opacity:0.5;text-align:center;">
|
||||
<svg height="96px" viewBox="0 0 24 24" width="96px">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4.27 3L3 4.27l9 9v.28c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4v-1.73L19.73 21 21 19.73 4.27 3zM14 7h4V3h-6v5.18l2 2z"/>
|
||||
</svg>
|
||||
<br/>No scrobbles yet!
|
||||
</td>
|
||||
</div>
|
||||
|
6
maloja/web/jinja/icons/remove_album.jinja
Normal file
6
maloja/web/jinja/icons/remove_album.jinja
Normal file
@ -0,0 +1,6 @@
|
||||
<div title="Remove from Album" id="removealbumicon" class="clickable_icon" onclick="removeAssociate(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M2 4.75C2 3.784 2.784 3 3.75 3h4.971a1.75 1.75 0 0 1 1.447.765l1.404 2.063a.25.25 0 0 0 .207.11h8.471c.966 0 1.75.783 1.75 1.75V19.25A1.75 1.75 0 0 1 20.25 21H4.75a.75.75 0 0 1 0-1.5h15.5a.25.25 0 0 0 .25-.25V7.688a.25.25 0 0 0-.25-.25h-8.471a1.751 1.751 0 0 1-1.447-.766L8.928 4.609a.252.252 0 0 0-.207-.109H3.75a.25.25 0 0 0-.25.25v3.5a.75.75 0 0 1-1.5 0v-3.5Z"></path>
|
||||
<path d="m 9.308 12.5 a 1 0.8 0 0 1 0 1.5 H 4.09 a 1 0.8 0 0 1 0 -1.5 h 5.218 Z"></path>
|
||||
</svg>
|
||||
</div>
|
5
maloja/web/jinja/icons/remove_artist.jinja
Normal file
5
maloja/web/jinja/icons/remove_artist.jinja
Normal file
@ -0,0 +1,5 @@
|
||||
<div title="Remove Artist" id="removeartisticon" class="clickable_icon" onclick="removeAssociate(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M 4 9.5 a 5 5 0 1 1 7.916 4.062 a 7.973 7.973 0 0 1 5.018 7.166 a 0.75 0.75 0 1 1 -1.499 0.044 a 6.469 6.469 0 0 0 -12.932 0 a 0.75 0.75 0 0 1 -1.499 -0.044 a 7.972 7.972 0 0 1 5.059 -7.181 A 4.994 4.994 0 0 1 4 9.5 Z M 9 6 a 3.5 3.5 0 1 0 0 7 a 3.5 3.5 0 0 0 0 -7 Z M 20 4 h 2.25 a 0.75 0.75 0 0 1 0 1.5 H 20 h -1.5 h -2.25 a 0.75 0.75 0 0 1 0 -1.5 h 2.25 h 0.75 Z"></path>
|
||||
</svg>
|
||||
</div>
|
56
maloja/web/jinja/partials/album_showcase.jinja
Normal file
56
maloja/web/jinja/partials/album_showcase.jinja
Normal file
@ -0,0 +1,56 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
|
||||
<div id="showcase_container">
|
||||
|
||||
|
||||
{% for entry in dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':True}) %}
|
||||
|
||||
{%- set cert = None -%}
|
||||
{%- if entry.scrobbles >= settings.scrobbles_gold_album -%}{% set cert = 'gold' %}{%- endif -%}
|
||||
{%- if entry.scrobbles >= settings.scrobbles_platinum_album -%}{% set cert = 'platinum' %}{%- endif -%}
|
||||
{%- if entry.scrobbles >= settings.scrobbles_diamond_album -%}{% set cert = 'diamond' %}{%- endif -%}
|
||||
|
||||
<table class="album">
|
||||
<tr><td> </td></tr>
|
||||
<tr><td>
|
||||
<a href="{{ links.url(entry.album) }}">
|
||||
<div class="shiny alwaysshiny certified_{{ cert }} lazy" data-bg="{{ images.get_album_image(entry.album) }}"'></div>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<span class="album_artists">{{ links.links(entry.album.artists) }}</span><br/>
|
||||
<span class="album_title">{{ links.link(entry.album) }}</span>
|
||||
</td></tr>
|
||||
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
||||
{% for entry in dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':False}) %}
|
||||
|
||||
|
||||
{% if artist not in (entry.album.artists or []) %}
|
||||
|
||||
{%- set cert = None -%}
|
||||
{%- if entry.scrobbles >= settings.scrobbles_gold_album -%}{% set cert = 'gold' %}{%- endif -%}
|
||||
{%- if entry.scrobbles >= settings.scrobbles_platinum_album -%}{% set cert = 'platinum' %}{%- endif -%}
|
||||
{%- if entry.scrobbles >= settings.scrobbles_diamond_album -%}{% set cert = 'diamond' %}{%- endif -%}
|
||||
|
||||
<table class="album">
|
||||
<tr><td>Appears on</td></tr>
|
||||
<tr><td>
|
||||
<a href="{{ links.url(entry.album) }}">
|
||||
<div class="shiny alwaysshiny certified_{{ cert }} lazy" data-bg="{{ images.get_album_image(entry.album) }}"'></div>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<span class="album_artists">{{ links.links(entry.album.artists) }}</span><br/>
|
||||
<span class="album_title">{{ links.link(entry.album) }}</span>
|
||||
</td></tr>
|
||||
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
73
maloja/web/jinja/partials/awards_album.jinja
Normal file
73
maloja/web/jinja/partials/awards_album.jinja
Normal file
@ -0,0 +1,73 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
{% macro medals(info) %}
|
||||
|
||||
<!-- MEDALS -->
|
||||
{% for year in info.medals.gold -%}
|
||||
<a title="Best Album in {{ year }}" class="hidelink medal shiny hovershiny gold" href='/charts_albums?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
{% for year in info.medals.silver -%}
|
||||
<a title="Second best Album in {{ year }}" class="hidelink medal shiny hovershiny silver" href='/charts_albums?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
{% for year in info.medals.bronze -%}
|
||||
<a title="Third best Album in {{ year }}" class="hidelink medal shiny hovershiny bronze" href='/charts_albums?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
|
||||
{% if info.medals.gold or info.medals.silver or info.medals.bronze %}
|
||||
<span class="spacer" ></span>
|
||||
{% endif %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% macro topweeks(info) %}
|
||||
|
||||
{% set encodedalbum = mlj_uri.uriencode({'album':info.album}) %}
|
||||
|
||||
<!-- TOPWEEKS -->
|
||||
<span>
|
||||
{% if info.topweeks > 0 -%}
|
||||
<a title="{{ info.topweeks }} weeks on #1" href="/performance?{{ encodedalbum }}&step=week">
|
||||
<img class="star" src="/media/star.png" />{{ info.topweeks }}</a>
|
||||
{%- endif %}
|
||||
</span>
|
||||
|
||||
{% if info.topweeks > 0 %}
|
||||
<span class="spacer" ></span>
|
||||
{% endif %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
|
||||
{% macro subcerts(album) %}
|
||||
|
||||
<!-- SUBCERTS -->
|
||||
|
||||
{% set charts = dbc.get_charts_tracks({'album':album.album,'timerange':malojatime.alltime()}) %}
|
||||
{% for e in charts -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_gold -%}{% set cert = 'gold' %}{%- endif -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_platinum -%}{% set cert = 'platinum' %}{%- endif -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_diamond -%}{% set cert = 'diamond' %}{%- endif -%}
|
||||
|
||||
{%- if cert -%}
|
||||
<a href='{{ links.url(e.track) }}' class="hidelink certified certified_{{ cert }} smallcerticon" title="{{ e.track.title }} has reached {{ cert.capitalize() }} status">
|
||||
{% include 'icons/cert_track.jinja' %}
|
||||
</a>
|
||||
{%- endif %}
|
||||
|
||||
{%- endfor %}
|
||||
|
||||
{%- endmacro %}
|
@ -5,21 +5,25 @@
|
||||
|
||||
<!-- MEDALS -->
|
||||
{% for year in info.medals.gold -%}
|
||||
<a title="Best Artist in {{ year }}" class="hidelink medal shiny gold" href='/charts_artists?max=50&in={{ year }}'>
|
||||
<a title="Best Artist in {{ year }}" class="hidelink medal shiny hovershiny gold" href='/charts_artists?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
{% for year in info.medals.silver -%}
|
||||
<a title="Second best Artist in {{ year }}" class="hidelink medal shiny silver" href='/charts_artists?max=50&in={{ year }}'>
|
||||
<a title="Second best Artist in {{ year }}" class="hidelink medal shiny hovershiny silver" href='/charts_artists?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
{% for year in info.medals.bronze -%}
|
||||
<a title="Third best Artist in {{ year }}" class="hidelink medal shiny bronze" href='/charts_artists?max=50&in={{ year }}'>
|
||||
<a title="Third best Artist in {{ year }}" class="hidelink medal shiny hovershiny bronze" href='/charts_artists?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
|
||||
{% if info.medals.gold or info.medals.silver or info.medals.bronze %}
|
||||
<span class="spacer" ></span>
|
||||
{% endif %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
@ -42,6 +46,10 @@
|
||||
{%- endif %}
|
||||
</span>
|
||||
|
||||
{% if info.topweeks > 0 %}
|
||||
<span class="spacer" ></span>
|
||||
{% endif %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
@ -52,22 +60,40 @@
|
||||
|
||||
|
||||
|
||||
{% macro certs(artist) %}
|
||||
{% macro subcerts(artist) %}
|
||||
|
||||
<!-- CERTS -->
|
||||
<!-- SUBCERTS -->
|
||||
|
||||
{% set charts = db.get_charts_tracks(artist=artist,timerange=malojatime.alltime()) %}
|
||||
|
||||
{% set albumcharts = dbc.get_charts_albums({'artist':artist,'timerange':malojatime.alltime(),'resolve_ids':True,'only_own_albums':True}) %}
|
||||
{% for e in albumcharts -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_gold_album -%}{% set cert = 'gold' %}{%- endif -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_platinum_album -%}{% set cert = 'platinum' %}{%- endif -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_diamond_album -%}{% set cert = 'diamond' %}{%- endif -%}
|
||||
|
||||
{%- if cert -%}
|
||||
<a href='{{ links.url(e.album) }}' class="hidelink certified certified_{{ cert }} smallcerticon" title="{{ e.album.albumtitle }} has reached {{ cert.capitalize() }} status">
|
||||
{% include 'icons/cert_album.jinja' %}
|
||||
</a>
|
||||
{%- endif %}
|
||||
|
||||
{%- endfor %}
|
||||
|
||||
|
||||
{% set charts = dbc.get_charts_tracks({'artist':artist,'timerange':malojatime.alltime()}) %}
|
||||
{% for e in charts -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_gold -%}{% set cert = 'gold' %}{%- endif -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_platinum -%}{% set cert = 'platinum' %}{%- endif -%}
|
||||
{%- if e.scrobbles >= settings.scrobbles_diamond -%}{% set cert = 'diamond' %}{%- endif -%}
|
||||
|
||||
{%- if cert -%}
|
||||
<a href='{{ links.url(e.track) }}'><img class="certrecord_small"
|
||||
src="/media/record_{{ cert }}.png"
|
||||
title="{{ e.track.title }} has reached {{ cert.capitalize() }} status" /></a>
|
||||
<a href='{{ links.url(e.track) }}' class="hidelink certified certified_{{ cert }} smallcerticon" title="{{ e.track.title }} has reached {{ cert.capitalize() }} status">
|
||||
{% include 'icons/cert_track.jinja' %}
|
||||
</a>
|
||||
{%- endif %}
|
||||
|
||||
{%- endfor %}
|
||||
|
||||
|
||||
|
||||
{%- endmacro %}
|
||||
|
@ -2,21 +2,25 @@
|
||||
|
||||
<!-- MEDALS -->
|
||||
{% for year in info.medals.gold -%}
|
||||
<a title="Best Track in {{ year }}" class="hidelink medal shiny gold" href='/charts_tracks?max=50&in={{ year }}'>
|
||||
<a title="Best Track in {{ year }}" class="hidelink medal shiny hovershiny gold" href='/charts_tracks?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
{% for year in info.medals.silver -%}
|
||||
<a title="Second best Track in {{ year }}" class="hidelink medal shiny silver" href='/charts_tracks?max=50&in={{ year }}'>
|
||||
<a title="Second best Track in {{ year }}" class="hidelink medal shiny hovershiny silver" href='/charts_tracks?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
{% for year in info.medals.bronze -%}
|
||||
<a title="Third best Track in {{ year }}" class="hidelink medal shiny bronze" href='/charts_tracks?max=50&in={{ year }}'>
|
||||
<a title="Third best Track in {{ year }}" class="hidelink medal shiny hovershiny bronze" href='/charts_tracks?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
|
||||
{% if info.medals.gold or info.medals.silver or info.medals.bronze %}
|
||||
<span class="spacer" ></span>
|
||||
{% endif %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
@ -39,25 +43,10 @@
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% macro certs(track) %}
|
||||
|
||||
<!-- CERTS -->
|
||||
|
||||
{% set info = db.track_info(track=track) %}
|
||||
{% if info.certification is not none %}
|
||||
<img class="certrecord"
|
||||
src="/media/record_{{ info.certification }}.png"
|
||||
title="This track has reached {{ info.certification.capitalize() }} status" />
|
||||
{% if info.topweeks > 0 %}
|
||||
<span class="spacer" ></span>
|
||||
{% endif %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
|
57
maloja/web/jinja/partials/charts_albums.jinja
Normal file
57
maloja/web/jinja/partials/charts_albums.jinja
Normal file
@ -0,0 +1,57 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'snippets/entityrow.jinja' as entityrow %}
|
||||
|
||||
{% if charts is undefined %}
|
||||
{% set charts = dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':False}) %}
|
||||
{% endif %}
|
||||
{% if compare %}
|
||||
{% if compare is true %}
|
||||
{% set compare = limitkeys.timerange.next(step=-1) %}
|
||||
{% if compare is none %}{% set compare = False %}{% endif %}
|
||||
{% endif %}
|
||||
{% if compare %}
|
||||
{% set prevalbums = dbc.get_charts_albums(filterkeys,{'timerange':compare}) %}
|
||||
|
||||
{% set lastranks = {} %}
|
||||
{% for t in prevalbums %}
|
||||
{% if lastranks.update({"|".join(t.album.artists or [])+"||"+t.album.albumtitle:t.rank}) %}{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for t in charts %}
|
||||
{% if "|".join(t.album.artists or [])+"||"+t.album.albumtitle in lastranks %}
|
||||
{% if t.update({'last_rank':lastranks["|".join(t.album.artists or [])+"||"+t.album.albumtitle]}) %}{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% set firstindex = amountkeys.page * amountkeys.perpage %}
|
||||
{% set lastindex = firstindex + amountkeys.perpage %}
|
||||
|
||||
|
||||
{% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %}
|
||||
<table class='list'>
|
||||
{% for e in charts %}
|
||||
{% if loop.index0 >= firstindex and loop.index0 < lastindex %}
|
||||
<tr class="listrow associateicons mergeicons" data-entity_id="{{ e['album_id'] }}" data-entity_type="album" data-entity_name="{{ e['album'].albumtitle }}">
|
||||
<!-- Rank -->
|
||||
<td class="rank">{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}</td>
|
||||
<!-- Rank change -->
|
||||
{% if compare %}
|
||||
{% if e.last_rank is undefined %}<td class='rankup' title='New'>🆕</td>
|
||||
{% elif e.last_rank < e.rank %}<td class='rankdown' title='Down from #{{ e.last_rank }}'>↘</td>
|
||||
{% elif e.last_rank > e.rank %}<td class='rankup' title='Up from #{{ e.last_rank }}'>↗</td>
|
||||
{% elif e.last_rank == e.rank %}<td class='ranksame' title='Unchanged'>➡</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- artist -->
|
||||
{{ entityrow.row(e['album'],adminmode=adminmode) }}
|
||||
|
||||
<!-- scrobbles -->
|
||||
<td class="amount">{{ links.link_scrobbles([{'album':e.album,'timerange':limitkeys.timerange}],amount=e['scrobbles']) }}</td>
|
||||
<td class="bar">{{ links.link_scrobbles([{'album':e.album,'timerange':limitkeys.timerange}],percent=e['scrobbles']*100/maxbar) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
31
maloja/web/jinja/partials/charts_albums_tiles.jinja
Normal file
31
maloja/web/jinja/partials/charts_albums_tiles.jinja
Normal file
@ -0,0 +1,31 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
|
||||
{% if charts is undefined %}
|
||||
{% set charts = dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':True}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set charts_14 = charts | fixlength(14) %}
|
||||
|
||||
|
||||
<div class="tiles">
|
||||
{% if charts_14[0] is none %}
|
||||
{% include 'icons/nodata.jinja' %}
|
||||
{% endif %}
|
||||
{% for entry in charts_14 %}
|
||||
{% if entry is not none %}
|
||||
{% set album = entry.album %}
|
||||
{% set rank = entry.rank %}
|
||||
<div class="tile">
|
||||
<a href="{{ links.url(album) }}">
|
||||
<div class="lazy" data-bg="{{ images.get_album_image(album) }}"'>
|
||||
<span class='stats'>#{{ rank }}</span> <span>{{ album.albumtitle }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
@ -2,7 +2,7 @@
|
||||
{% import 'snippets/entityrow.jinja' as entityrow %}
|
||||
|
||||
{% if charts is undefined %}
|
||||
{% set charts = dbc.get_charts_artists(limitkeys) %}
|
||||
{% set charts = dbc.get_charts_artists(limitkeys,specialkeys) %}
|
||||
{% endif %}
|
||||
|
||||
{% if compare %}
|
||||
@ -11,7 +11,7 @@
|
||||
{% if compare is none %}{% set compare = False %}{% endif %}
|
||||
{% endif %}
|
||||
{% if compare %}
|
||||
{% set prevartists = dbc.get_charts_artists({'timerange':compare}) %}
|
||||
{% set prevartists = dbc.get_charts_artists({'timerange':compare},specialkeys) %}
|
||||
|
||||
{% set lastranks = {} %}
|
||||
{% for a in prevartists %}
|
||||
@ -30,12 +30,13 @@
|
||||
{% set lastindex = firstindex + amountkeys.perpage %}
|
||||
|
||||
|
||||
|
||||
{% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %}
|
||||
|
||||
<table class='list'>
|
||||
{% for e in charts %}
|
||||
{% if loop.index0 >= firstindex and loop.index0 < lastindex %}
|
||||
<tr>
|
||||
<tr class="listrow associateicons mergeicons" data-entity_id="{{ e['artist_id'] }}" data-entity_type="artist" data-entity_name="{{ e['artist'] }}">
|
||||
<!-- Rank -->
|
||||
<td class="rank">{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}</td>
|
||||
<!-- Rank change -->
|
||||
@ -48,11 +49,16 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- artist -->
|
||||
{{ entityrow.row(e['artist']) }}
|
||||
{{ entityrow.row(e['artist'],adminmode=adminmode,counting=([] if specialkeys.separate else e.associated_artists)) }}
|
||||
|
||||
<!-- scrobbles -->
|
||||
<td class="amount">{{ links.link_scrobbles([{'artist':e['artist'],'associated':True,'timerange':limitkeys.timerange}],amount=e['scrobbles']) }}</td>
|
||||
<td class="bar">{{ links.link_scrobbles([{'artist':e['artist'],'associated':True,'timerange':limitkeys.timerange}],percent=e['scrobbles']*100/maxbar) }}</td>
|
||||
<td class="amount">{{ links.link_scrobbles([{'artist':e['artist'],'associated':(not specialkeys.separate),'timerange':limitkeys.timerange}],amount=e['scrobbles']) }}</td>
|
||||
<td class="bar">
|
||||
{{ links.link_scrobbles([{'artist':e['artist'],'associated':False,'timerange':limitkeys.timerange}],percent=e['real_scrobbles']*100/maxbar) }}
|
||||
{%- if e['real_scrobbles'] != e['scrobbles'] -%}
|
||||
{{ links.link_scrobbles([{'artist':e['artist'],'associated':True,'timerange':limitkeys.timerange}],percent=(e['scrobbles']-e['real_scrobbles'])*100/maxbar) }}
|
||||
{%- endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
@ -6,40 +6,26 @@
|
||||
{% endif %}
|
||||
|
||||
{% set charts_14 = charts | fixlength(14) %}
|
||||
{% set charts_cycler = cycler(*charts_14) %}
|
||||
|
||||
|
||||
|
||||
<table class="tiles_top"><tr>
|
||||
{% for segment in range(3) %}
|
||||
{% if charts_14[0] is none and loop.first %}
|
||||
{% include 'icons/nodata.jinja' %}
|
||||
{% else %}
|
||||
<td>
|
||||
{% set segmentsize = segment+1 %}
|
||||
<table class="tiles_{{ segmentsize }}x{{ segmentsize }} tiles_sub">
|
||||
{% for row in range(segmentsize) -%}
|
||||
<tr>
|
||||
{% for col in range(segmentsize) %}
|
||||
{% set entry = charts_cycler.next() %}
|
||||
{% if entry is not none %}
|
||||
{% set artist = entry.artist %}
|
||||
{% set rank = entry.rank %}
|
||||
<td>
|
||||
<a href="{{ links.url(artist) }}">
|
||||
<div class="lazy" data-bg="{{ images.get_artist_image(artist) }}"'>
|
||||
<span class='stats'>#{{ rank }}</span> <span>{{ artist }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
{% else -%}
|
||||
<td></td>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</table>
|
||||
</td>
|
||||
<div class="tiles">
|
||||
{% if charts_14[0] is none %}
|
||||
{% include 'icons/nodata.jinja' %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr></table>
|
||||
{% for entry in charts_14 %}
|
||||
{% if entry is not none %}
|
||||
{% set artist = entry.artist %}
|
||||
{% set rank = entry.rank %}
|
||||
<div class="tile">
|
||||
<a href="{{ links.url(artist) }}">
|
||||
<div class="lazy" data-bg="{{ images.get_artist_image(artist) }}"'>
|
||||
<span class='stats'>#{{ rank }}</span> <span>{{ artist }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
@ -28,11 +28,12 @@
|
||||
{% set firstindex = amountkeys.page * amountkeys.perpage %}
|
||||
{% set lastindex = firstindex + amountkeys.perpage %}
|
||||
|
||||
|
||||
{% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %}
|
||||
<table class='list'>
|
||||
{% for e in charts %}
|
||||
{% if loop.index0 >= firstindex and loop.index0 < lastindex %}
|
||||
<tr>
|
||||
<tr class="listrow associateicons mergeicons" data-entity_id="{{ e['track_id'] }}" data-entity_type="track" data-entity_name="{{ e['track'].title }}">
|
||||
<!-- Rank -->
|
||||
<td class="rank">{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}</td>
|
||||
<!-- Rank change -->
|
||||
@ -45,7 +46,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- artist -->
|
||||
{{ entityrow.row(e['track']) }}
|
||||
{{ entityrow.row(e['track'],adminmode=adminmode) }}
|
||||
|
||||
<!-- scrobbles -->
|
||||
<td class="amount">{{ links.link_scrobbles([{'track':e.track,'timerange':limitkeys.timerange}],amount=e['scrobbles']) }}</td>
|
||||
|
@ -6,39 +6,26 @@
|
||||
{% endif %}
|
||||
|
||||
{% set charts_14 = charts | fixlength(14) %}
|
||||
{% set charts_cycler = cycler(*charts_14) %}
|
||||
|
||||
|
||||
<table class="tiles_top"><tr>
|
||||
{% for segment in range(3) %}
|
||||
{% if charts_14[0] is none and loop.first %}
|
||||
{% include 'icons/nodata.jinja' %}
|
||||
{% else %}
|
||||
<td>
|
||||
{% set segmentsize = segment+1 %}
|
||||
<table class="tiles_{{ segmentsize }}x{{ segmentsize }} tiles_sub">
|
||||
{% for row in range(segmentsize) -%}
|
||||
<tr>
|
||||
{% for col in range(segmentsize) %}
|
||||
{% set entry = charts_cycler.next() %}
|
||||
{% if entry is not none %}
|
||||
{% set track = entry.track %}
|
||||
{% set rank = entry.rank %}
|
||||
<td>
|
||||
<a href="{{ links.url(track) }}">
|
||||
<div class="lazy" data-bg="{{ images.get_track_image(track) }}")'>
|
||||
<span class='stats'>#{{ rank }}</span> <span>{{ track.title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
{% else -%}
|
||||
<td></td>
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</table>
|
||||
</td>
|
||||
<div class="tiles">
|
||||
{% if charts_14[0] is none %}
|
||||
{% include 'icons/nodata.jinja' %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr></table>
|
||||
{% for entry in charts_14 %}
|
||||
{% if entry is not none %}
|
||||
{% set track = entry.track %}
|
||||
{% set rank = entry.rank %}
|
||||
<div class="tile">
|
||||
<a href="{{ links.url(track) }}">
|
||||
<div class="lazy" data-bg="{{ images.get_track_image(track) }}"'>
|
||||
<span class='stats'>#{{ rank }}</span> <span>{{ track.title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
45
maloja/web/jinja/partials/info_album.jinja
Normal file
45
maloja/web/jinja/partials/info_album.jinja
Normal file
@ -0,0 +1,45 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'partials/awards_album.jinja' as awards with context %}
|
||||
|
||||
{% set album = filterkeys.album %}
|
||||
{% set info = dbc.album_info({'album':album}) %}
|
||||
{% set encodedalbum = mlj_uri.uriencode({'album':album}) %}
|
||||
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
{% if adminmode %}
|
||||
<div
|
||||
class="shiny alwaysshiny changeable-image" data-uploader="b64=>upload('{{ encodedalbum }}',b64)"
|
||||
style="background-image:url('{{ images.get_album_image(info.album) }}');"
|
||||
title="Drag & Drop to upload new image"
|
||||
></div>
|
||||
{% else %}
|
||||
<div class="shiny alwaysshiny" style="background-image:url('{{ images.get_album_image(album) }}');">
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text">
|
||||
<span>{{ links.links(album.artists) }}</span><br/>
|
||||
{% if condensed %}<a href="/album?{{ encodedalbum }}">{% endif %}
|
||||
<h1 id="main_entity_name" class="headerwithextra">{{ info.album.albumtitle | e }}</h1>
|
||||
{%- if condensed -%}</a>{% endif %}
|
||||
<span class="rank"><a href="/charts_albums?max=100">#{{ info.position }}</a></span>
|
||||
<br/>
|
||||
|
||||
<p class="stats">
|
||||
<a href="{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}">{{ info['scrobbles'] }} Scrobbles</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{ awards.medals(info) }}
|
||||
{{ awards.topweeks(info) }}
|
||||
{{ awards.subcerts(info) }}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
62
maloja/web/jinja/partials/info_artist.jinja
Normal file
62
maloja/web/jinja/partials/info_artist.jinja
Normal file
@ -0,0 +1,62 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'partials/awards_artist.jinja' as awards with context %}
|
||||
|
||||
|
||||
{% set artist = filterkeys.artist %}
|
||||
{% set info = dbc.artist_info({'artist':artist}) %}
|
||||
{% set encodedartist = mlj_uri.uriencode({'artist':artist}) %}
|
||||
|
||||
{% set credited = info.get('replace') %}
|
||||
{% set included = info.get('associated') %}
|
||||
|
||||
{% if credited is not none %}
|
||||
{% set competes = false %}
|
||||
{% else %}
|
||||
{% set credited = artist %}
|
||||
{% set competes = true %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
{% if adminmode %}
|
||||
<div
|
||||
class="shiny alwaysshiny changeable-image" data-uploader="b64=>upload('{{ encodedartist }}',b64)"
|
||||
style="background-image:url('{{ images.get_artist_image(info.artist) }}');"
|
||||
title="Drag & Drop to upload new image"
|
||||
></div>
|
||||
{% else %}
|
||||
<div class="shiny alwaysshiny" style="background-image:url('{{ images.get_artist_image(artist) }}');">
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text">
|
||||
{% if condensed %}<a href="/artist?{{ encodedartist }}">{% endif %}
|
||||
<h1 id="main_entity_name" class="headerwithextra">{{ info.artist | e }}</h1>
|
||||
{%- if condensed -%}</a>{% endif %}
|
||||
{% if competes and info['scrobbles']>0 %}<span class="rank"><a href="/charts_artists?max=100">#{{ info.position }}</a></span>{% endif %}
|
||||
<br/>
|
||||
{% if competes and included and (not condensed) %}
|
||||
<span>associated: {{ links.links(included) }}</span>
|
||||
{% elif not competes %}
|
||||
<span>Competing under {{ links.link(credited) }} (#{{ info.position }})</span>
|
||||
{% endif %}
|
||||
|
||||
<p class="stats">
|
||||
<a href="{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}">{{ info['scrobbles'] }} Scrobbles</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
{% if competes %}
|
||||
{{ awards.medals(info) }}
|
||||
{{ awards.topweeks(info) }}
|
||||
{% endif %}
|
||||
{{ awards.subcerts(artist) }}
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
49
maloja/web/jinja/partials/info_track.jinja
Normal file
49
maloja/web/jinja/partials/info_track.jinja
Normal file
@ -0,0 +1,49 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
{% set track = filterkeys.track %}
|
||||
{% set info = dbc.track_info({'track':track}) %}
|
||||
{% set encodedtrack = mlj_uri.uriencode({'track':track}) %}
|
||||
|
||||
{% import 'partials/awards_track.jinja' as awards with context %}
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
{% if adminmode %}
|
||||
<div
|
||||
class="shiny alwaysshiny changeable-image" data-uploader="b64=>upload('{{ encodedtrack }}',b64)"
|
||||
style="background-image:url('{{ images.get_track_image(info.track) }}');"
|
||||
title="Drag & Drop to upload new image"
|
||||
></div>
|
||||
{% else %}
|
||||
<div class="shiny alwaysshiny" style="background-image:url('{{ images.get_track_image(track) }}');">
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text">
|
||||
<span>{{ links.links(track.artists) }}</span><br/>
|
||||
{% if condensed %}<a href="/track?{{ encodedtrack }}">{% endif %}
|
||||
<h1 id="main_entity_name" class="headerwithextra">{{ info.track.title | e }}</h1>
|
||||
{%- if condensed -%}</a>{% endif %}
|
||||
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
|
||||
<br/>
|
||||
{% if info.track.album %}
|
||||
from {{ links.link(info.track.album) }}<br/>
|
||||
{% endif %}
|
||||
|
||||
<p class="stats">
|
||||
{% if adminmode %}<button type="button" onclick="scrobbleThisTrack()">Scrobble now</button>{% endif %}
|
||||
<a href="{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}">{{ info['scrobbles'] }} Scrobbles</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{ awards.medals(info) }}
|
||||
{{ awards.topweeks(info) }}
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
27
maloja/web/jinja/partials/list_tracks.jinja
Normal file
27
maloja/web/jinja/partials/list_tracks.jinja
Normal file
@ -0,0 +1,27 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'snippets/entityrow.jinja' as entityrow %}
|
||||
|
||||
|
||||
|
||||
{% set firstindex = amountkeys.page * amountkeys.perpage %}
|
||||
{% set lastindex = firstindex + amountkeys.perpage %}
|
||||
|
||||
|
||||
<table class='list'>
|
||||
{% for e in list %}
|
||||
{% if loop.index0 >= firstindex and loop.index0 < lastindex %}
|
||||
<tr class="listrow associateicons mergeicons" data-entity_id="{{ e['track_id'] }}" data-entity_type="track" data-entity_name="{{ e['track'].title }}">
|
||||
|
||||
<!-- artist -->
|
||||
{{ entityrow.row(e['track'],adminmode=adminmode) }}
|
||||
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
</script>
|
@ -1,6 +1,6 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
{% set ranges = dbc.get_performance(filterkeys,limitkeys,delimitkeys) %}
|
||||
{% set ranges = dbc.get_performance(filterkeys,limitkeys,delimitkeys,specialkeys) %}
|
||||
|
||||
{% set minrank = ranges|map(attribute="rank")|reject("none")|max|default(60) %}
|
||||
{% set minrank = minrank + 20 %}
|
||||
@ -11,13 +11,13 @@
|
||||
|
||||
{% set thisrange = t.range %}
|
||||
<tr>
|
||||
<td>{{ thisrange.desc() }}</td>
|
||||
<td class="timerange">{{ thisrange.desc() }}</td>
|
||||
<td class="rank">
|
||||
{{ links.link_rank(filterkeys,thisrange,rank=t.rank) }}
|
||||
{{ links.link_rank(filterkeys,specialkeys,thisrange,rank=t.rank) }}
|
||||
</td>
|
||||
<td class="chart">
|
||||
{% set prct = ((minrank+1-t.rank)*100/minrank if t.rank is not none else 0) %}
|
||||
{{ links.link_rank(filterkeys,thisrange,percent=prct,rank=t.rank) }}
|
||||
{{ links.link_rank(filterkeys,specialkeys,thisrange,percent=prct,rank=t.rank) }}
|
||||
</td>
|
||||
|
||||
|
||||
|
@ -10,12 +10,19 @@
|
||||
|
||||
{% set thisrange = t.range %}
|
||||
<tr>
|
||||
<td>{{ thisrange.desc() }}</td>
|
||||
<td class="timerange">{{ thisrange.desc() }}</td>
|
||||
<td class="amount">
|
||||
{{ links.link_scrobbles([filterkeys,{'timerange':thisrange}],amount=t.scrobbles) }}
|
||||
</td>
|
||||
<td class="bar">
|
||||
{{ links.link_scrobbles([filterkeys,{'timerange':thisrange}],percent=t.scrobbles*100/maxbar) }}
|
||||
{% if 'artist' in filterkeys and filterkeys.get('associated') %}
|
||||
{{ links.link_scrobbles([{'artist':filterkeys.artist,'associated':False,'timerange':thisrange}],percent=t.real_scrobbles*100/maxbar) }}
|
||||
{%- if t.real_scrobbles != t.scrobbles -%}
|
||||
{{ links.link_scrobbles([{'artist':filterkeys.artist,'associated':True,'timerange':thisrange}],percent=(t.scrobbles-t.real_scrobbles)*100/maxbar) }}
|
||||
{%- endif %}
|
||||
{% else %}
|
||||
{{ links.link_scrobbles([filterkeys,{'timerange':thisrange}],percent=t.scrobbles*100/maxbar) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -6,12 +6,8 @@
|
||||
{% import 'snippets/entityrow.jinja' as entityrow %}
|
||||
|
||||
|
||||
<script src="/edit.js"></script>
|
||||
|
||||
|
||||
<table class='list'>
|
||||
{% for s in scrobbles -%}
|
||||
{%- if loop.index0 >= firstindex and loop.index0 < lastindex -%}
|
||||
<tr>
|
||||
<td class='time'>{{ malojatime.timestamp_desc(s["time"],short=shortTimeDesc) }}</td>
|
||||
{{ entityrow.row(s.track) }}
|
||||
@ -44,6 +40,5 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
30
maloja/web/jinja/partials/top_albums.jinja
Normal file
30
maloja/web/jinja/partials/top_albums.jinja
Normal file
@ -0,0 +1,30 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'snippets/entityrow.jinja' as entityrow %}
|
||||
|
||||
{% set ranges = dbc.get_top_albums(filterkeys,limitkeys,delimitkeys) %}
|
||||
|
||||
{% set maxbar = ranges|map(attribute="scrobbles")|max|default(1) %}
|
||||
{% if maxbar < 1 %}{% set maxbar = 1 %}{% endif %}
|
||||
|
||||
<table class="list">
|
||||
{% for e in ranges %}
|
||||
|
||||
{% set thisrange = e.range %}
|
||||
{% set album = e.album %}
|
||||
<tr>
|
||||
<td><a href="{{ mlj_uri.create_uri("/charts_albums",{'timerange':thisrange}) }}">{{ thisrange.desc() }}</a></td>
|
||||
|
||||
{% if album is none %}
|
||||
<td><div></div></td>
|
||||
<td class='stats'>n/a</td>
|
||||
<td class='amount'>0</td>
|
||||
<td class='bar'></td>
|
||||
{% else %}
|
||||
{{ entityrow.row(album) }}
|
||||
<td class='amount'>{{ links.link_scrobbles([{'album':album,'timerange':thisrange}],amount=e.scrobbles) }}</td>
|
||||
<td class='bar'> {{ links.link_scrobbles([{'album':album,'timerange':thisrange}],percent=e.scrobbles*100/maxbar) }}</td>
|
||||
{% endif %}
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
@ -1,7 +1,7 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'snippets/entityrow.jinja' as entityrow %}
|
||||
|
||||
{% set ranges = dbc.get_top_artists(limitkeys,delimitkeys) %}
|
||||
{% set ranges = dbc.get_top_artists(limitkeys,delimitkeys,specialkeys) %}
|
||||
|
||||
{% set maxbar = ranges|map(attribute="scrobbles")|max|default(1) %}
|
||||
{% if maxbar < 1 %}{% set maxbar = 1 %}{% endif %}
|
||||
@ -12,7 +12,7 @@
|
||||
{% set thisrange = e.range %}
|
||||
{% set artist = e.artist %}
|
||||
<tr>
|
||||
<td>{{ thisrange.desc() }}</td>
|
||||
<td><a href="{{ mlj_uri.create_uri("/charts_artists",{'timerange':thisrange},specialkeys) }}">{{ thisrange.desc() }}</a></td>
|
||||
|
||||
{% if artist is none %}
|
||||
<td><div></div></td>
|
||||
@ -20,9 +20,14 @@
|
||||
<td class='amount'>0</td>
|
||||
<td class='bar'></td>
|
||||
{% else %}
|
||||
{{ entityrow.row(artist) }}
|
||||
<td class='amount'>{{ links.link_scrobbles([{'artist':artist,'timerange':thisrange}],amount=e.scrobbles) }}</td>
|
||||
<td class='bar'> {{ links.link_scrobbles([{'artist':artist,'timerange':thisrange}],percent=e.scrobbles*100/maxbar) }}</td>
|
||||
{{ entityrow.row(artist,counting=([] if specialkeys.separate else e.associated_artists)) }}
|
||||
<td class='amount'>{{ links.link_scrobbles([{'artist':artist,'associated':(not specialkeys.separate),'timerange':thisrange}],amount=e.scrobbles) }}</td>
|
||||
<td class='bar'>
|
||||
{{ links.link_scrobbles([{'artist':e['artist'],'associated':False,'timerange':e.range}],percent=e['real_scrobbles']*100/maxbar) }}
|
||||
{%- if e['real_scrobbles'] != e['scrobbles'] -%}
|
||||
{{ links.link_scrobbles([{'artist':e['artist'],'associated':True,'timerange':e.range}],percent=(e['scrobbles']-e['real_scrobbles'])*100/maxbar) }}
|
||||
{%- endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
</tr>
|
||||
|
@ -12,7 +12,7 @@
|
||||
{% set thisrange = e.range %}
|
||||
{% set track = e.track %}
|
||||
<tr>
|
||||
<td>{{ thisrange.desc() }}</td>
|
||||
<td><a href="{{ mlj_uri.create_uri("/charts_tracks",{'timerange':thisrange}) }}">{{ thisrange.desc() }}</a></td>
|
||||
|
||||
{% if track is none %}
|
||||
<td><div></div></td>
|
||||
|
@ -25,7 +25,9 @@
|
||||
<br/>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys,prefix='of') }}
|
||||
<br/><br/>
|
||||
{% with artistchart = (filterkeys.get('artist') is not none) %}
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -4,8 +4,9 @@
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
{% import 'snippets/pagination.jinja' as pagination %}
|
||||
|
||||
{% set totalscrobbles = dbc.get_scrobbles_num(filterkeys,limitkeys) %}
|
||||
{% set scrobbles = dbc.get_scrobbles(filterkeys,limitkeys,amountkeys) %}
|
||||
{% set pages = math.ceil(scrobbles.__len__() / amountkeys.perpage) %}
|
||||
{% set pages = math.ceil(totalscrobbles / amountkeys.perpage) %}
|
||||
|
||||
{% if filterkeys.get('track') is not none %}
|
||||
{% set img = images.get_track_image(filterkeys.track) %}
|
||||
@ -29,7 +30,7 @@
|
||||
<h1>Scrobbles</h1><br/>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
<br/>
|
||||
<p class="stats">{{ scrobbles.__len__() }} Scrobbles</p>
|
||||
<p class="stats">{{ totalscrobbles }} Scrobbles</p>
|
||||
<br/>
|
||||
{% with delimitkeys = {} %}
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
|
@ -1,9 +1,11 @@
|
||||
{% macro row(entity,counting=[]) %}
|
||||
{% macro row(entity,counting=[],adminmode=False) %}
|
||||
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
{% if entity is mapping and 'artists' in entity %}
|
||||
{% if entity is mapping and 'title' in entity %}
|
||||
{% set img = images.get_track_image(entity) %}
|
||||
{% elif entity is mapping and 'albumtitle' in entity %}
|
||||
{% set img = images.get_album_image(entity) %}
|
||||
{% else %}
|
||||
{% set img = images.get_artist_image(entity) %}
|
||||
{% endif %}
|
||||
@ -20,6 +22,10 @@
|
||||
<td class='track'>
|
||||
<span class='artist_in_trackcolumn'>{{ links.links(entity.artists) }}</span> – {{ links.link(entity) }}
|
||||
</td>
|
||||
{% elif entity is mapping and 'albumtitle' in entity %}
|
||||
<td class='album'>
|
||||
<span class='artist_in_trackcolumn'>{{ links.links(entity.artists) }}</span> – {{ links.link(entity) }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class='artist'>{{ links.link(entity) }}
|
||||
{% if counting != [] %}
|
||||
@ -29,4 +35,16 @@
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if adminmode %}
|
||||
<td>
|
||||
{% include 'icons/merge_mark.jinja' %}
|
||||
{% include 'icons/merge_unmark.jinja' %}
|
||||
{% if (entity is mapping) %}
|
||||
{% include 'icons/association_mark.jinja' %}
|
||||
{% include 'icons/association_unmark.jinja' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{% endmacro %}
|
||||
|
@ -3,10 +3,13 @@
|
||||
{% macro desc(filterkeys,limitkeys,prefix="by") %}
|
||||
|
||||
{% if filterkeys.get('artist') is not none %}
|
||||
{{ prefix }} {{ links.link(filterkeys.get('artist')) }}
|
||||
{{ prefix }} {{ links.link(filterkeys.get('artist')) }}{% if filterkeys.get('associated') %} (and associated artists){% endif %}
|
||||
{% elif filterkeys.get('track') is not none %}
|
||||
of {{ links.link(filterkeys.get('track')) }}
|
||||
by {{ links.links(filterkeys["track"]["artists"]) }}
|
||||
{% elif filterkeys.get('album') is not none %}
|
||||
from {{ links.link(filterkeys.get('album')) }}
|
||||
by {{ links.links(filterkeys["album"]["artists"]) }}
|
||||
{% endif %}
|
||||
{{ limitkeys.timerange.desc(prefix=True) }}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% macro link(entity) -%}
|
||||
{% if entity is mapping and 'artists' in entity %}
|
||||
{% set name = entity.title %}
|
||||
{% if entity is mapping and ('title' in entity or 'albumtitle' in entity) %}
|
||||
{% set name = entity.title or entity.albumtitle %}
|
||||
{% else %}
|
||||
{% set name = entity %}
|
||||
{% endif %}
|
||||
@ -9,15 +9,21 @@
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro links(entities) -%}
|
||||
{% for entity in entities -%}
|
||||
{{ link(entity) }}{{ ", " if not loop.last }}
|
||||
{%- endfor %}
|
||||
{% if entities is none or entities == [] %}
|
||||
{{ settings["DEFAULT_ALBUM_ARTIST"] }}
|
||||
{% else %}
|
||||
{% for entity in entities -%}
|
||||
{{ link(entity) }}{{ ", " if not loop.last }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
|
||||
{% macro url(entity) %}
|
||||
{% if entity is mapping and 'artists' in entity -%}
|
||||
{% if entity is mapping and 'albumtitle' in entity -%}
|
||||
{{ mlj_uri.create_uri("/album",{'album':entity}) }}
|
||||
{% elif entity is mapping and 'artists' in entity -%}
|
||||
{{ mlj_uri.create_uri("/track",{'track':entity}) }}
|
||||
{%- else -%}
|
||||
{{ mlj_uri.create_uri("/artist",{'artist':entity}) }}
|
||||
@ -39,12 +45,14 @@
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
{% macro link_rank(filterkeys,timerange,rank=None,percent=None) %}
|
||||
{% macro link_rank(filterkeys,specialkeys,timerange,rank=None,percent=None) %}
|
||||
|
||||
{% if 'track' in filterkeys %}
|
||||
{% set url = mlj_uri.create_uri("/charts_tracks",{'timerange':timerange}) %}
|
||||
{% elif 'album' in filterkeys %}
|
||||
{% set url = mlj_uri.create_uri("/charts_albums",{'timerange':timerange}) %}
|
||||
{% elif 'artist' in filterkeys %}
|
||||
{% set url = mlj_uri.create_uri("/charts_artists",{'timerange':timerange}) %}
|
||||
{% set url = mlj_uri.create_uri("/charts_artists",{'timerange':timerange},specialkeys) %}
|
||||
{% endif %}
|
||||
|
||||
{% set rankclass = {1:'gold',2:'silver',3:'bronze'}[rank] or "" %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
|
||||
{% set allkeys = [filterkeys,limitkeys,delimitkeys,amountkeys] | combine_dicts %}
|
||||
{% set allkeys = [filterkeys,limitkeys,delimitkeys,amountkeys,specialkeys] | combine_dicts %}
|
||||
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
{% set nextrange = thisrange.next(1) %}
|
||||
|
||||
{% if prevrange is not none %}
|
||||
<a href='{{ mlj_uri.create_uri("",filterkeys,limitkeys,delimitkeys,{'timerange':prevrange}) }}'><span class='stat_selector'>{{ prevrange.desc() }}</span></a>
|
||||
<a href='{{ mlj_uri.create_uri("",filterkeys,limitkeys,delimitkeys,specialkeys,{'timerange':prevrange}) }}'><span class='stat_selector'>{{ prevrange.desc() }}</span></a>
|
||||
«
|
||||
{% endif %}
|
||||
{% if prevrange is not none or nextrange is not none %}
|
||||
@ -21,7 +21,7 @@
|
||||
{% endif %}
|
||||
{% if nextrange is not none %}
|
||||
»
|
||||
<a href='{{ mlj_uri.create_uri("",filterkeys,limitkeys,delimitkeys,{'timerange':nextrange}) }}'><span class='stat_selector'>{{ nextrange.desc() }}</span></a>
|
||||
<a href='{{ mlj_uri.create_uri("",filterkeys,limitkeys,delimitkeys,specialkeys,{'timerange':nextrange}) }}'><span class='stat_selector'>{{ nextrange.desc() }}</span></a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
@ -61,3 +61,32 @@
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<!-- we do this only for things filtered by artist (to include or not include their subunits etc), but NOT for get_performance
|
||||
since it's more intuitive to fall under the separate/combined logic below -->
|
||||
{% if ('artist' in filterkeys) and (not artistchart) %}
|
||||
<div>
|
||||
{% for o in xassociated %}
|
||||
{% if o.replacekeys | map('compare_key_in_dicts',o.replacekeys,allkeys) | alltrue %}
|
||||
<span style='opacity:0.5;'>{{ o.localisation }}</span>
|
||||
{% else %}
|
||||
<a href='{{ mlj_uri.create_uri("",allkeys,o.replacekeys) }}'><span>{{ o.localisation }}</span></a>
|
||||
{% endif %}
|
||||
{{ "|" if not loop.last }}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if artistchart %}
|
||||
<div>
|
||||
{% for o in xseparate %}
|
||||
{% if o.replacekeys | map('compare_key_in_dicts',o.replacekeys,allkeys) | alltrue %}
|
||||
<span style='opacity:0.5;'>{{ o.localisation }}</span>
|
||||
{% else %}
|
||||
<a href='{{ mlj_uri.create_uri("",allkeys,o.replacekeys) }}'><span>{{ o.localisation }}</span></a>
|
||||
{% endif %}
|
||||
{{ "|" if not loop.last }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -3,109 +3,28 @@
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
<script>document.addEventListener('DOMContentLoaded',function() {
|
||||
showRange('topartists','{{ settings["DEFAULT_RANGE_CHARTS_ARTISTS"] }}');
|
||||
showRange('toptracks','{{ settings["DEFAULT_RANGE_CHARTS_TRACKS"] }}');
|
||||
showRange('pulse','{{ settings["DEFAULT_STEP_PULSE"] }}');
|
||||
})</script>
|
||||
<script src="/rangeselect.js"></script>
|
||||
<script src="/cookies.js"></script>
|
||||
<script src="/statselect.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/static/css/startpage.css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
|
||||
<!-- ARTIST CHARTS -->
|
||||
<h1><a class="stat_link_topartists" href="/charts_artists?in=alltime">Top Artists</a></h1>
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<span onclick="showRangeManual('topartists','{{ r.identifier }}')" class="stat_selector_topartists selector_topartists_{{ r.identifier }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{{ "|" if not loop.last }}
|
||||
{%- endfor %}
|
||||
|
||||
<br/><br/>
|
||||
<div id="startpage">
|
||||
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<span class="stat_module_topartists topartists_{{ r.identifier }}" style="display:none;">
|
||||
{%- with limitkeys = {"timerange":r.range} -%}
|
||||
{% include 'partials/charts_artists_tiles.jinja' %}
|
||||
{%- endwith -%}
|
||||
</span>
|
||||
{%- endfor %}
|
||||
{% for module in ['charts_artists','charts_tracks','charts_albums','pulse','lastscrobbles'] %}
|
||||
<section id="start_page_module_{{ module }}" style="grid-area: {{ module }}">
|
||||
<!-- MODULE: {{ module }} -->
|
||||
|
||||
{% include 'startpage_modules/' + module + '.jinja' %}
|
||||
|
||||
|
||||
<!-- END MODULE: {{ module }} -->
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TRACK CHARTS -->
|
||||
<h1><a class="stat_link_toptracks" href="/charts_tracks?in=alltime">Top Tracks</a></h1>
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<span onclick="showRangeManual('toptracks','{{ r.identifier }}')" class="stat_selector_toptracks selector_toptracks_{{ r.identifier }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{{ "|" if not loop.last }}
|
||||
{%- endfor %}
|
||||
|
||||
<br/><br/>
|
||||
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<span class="stat_module_toptracks toptracks_{{ r.identifier }}" style="display:none;">
|
||||
{%- with limitkeys = {"timerange":r.range} -%}
|
||||
{% include 'partials/charts_tracks_tiles.jinja' %}
|
||||
{%- endwith -%}
|
||||
</span>
|
||||
{%- endfor %}
|
||||
|
||||
|
||||
<div class="sidelist">
|
||||
<!-- SCROBBLES -->
|
||||
|
||||
<h1><a href="/scrobbles">Last Scrobbles</a></h1>
|
||||
|
||||
{% for range in xcurrent %}
|
||||
<span class="stats">{{ range.localisation }}</span>
|
||||
<a href='/scrobbles?in={{ range.identifier }}'>{{ dbc.get_scrobbles_num({'timerange':range.range}) }}</a>
|
||||
{% endfor %}
|
||||
<br/><br/>
|
||||
|
||||
<span class="stat_module">
|
||||
|
||||
|
||||
{%- with amountkeys = {"perpage":12,"page":0}, shortTimeDesc=True -%}
|
||||
{% include 'partials/scrobbles.jinja' %}
|
||||
{%- endwith -%}
|
||||
</span>
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- PULSE -->
|
||||
<h1><a class="stat_link_pulse" href="/pulse?trail=1&step=month">Pulse</a></h1>
|
||||
|
||||
{% for range in xranges -%}
|
||||
<span onclick="showRangeManual('pulse','{{ range.identifier }}')" class="stat_selector_pulse selector_pulse_{{ range.identifier }}">
|
||||
{{ range.localisation }}
|
||||
</span>
|
||||
{{ "|" if not loop.last }}
|
||||
{%- endfor %}
|
||||
<br/><br/>
|
||||
|
||||
{% for range in xranges -%}
|
||||
<span class="stat_module_pulse pulse_{{ range.identifier }}" style="display:none;">
|
||||
{%- with limitkeys={"since":range.firstrange},delimitkeys={"step":range.identifier} -%}
|
||||
{% include 'partials/pulse.jinja' %}
|
||||
{%- endwith -%}
|
||||
</span>
|
||||
{%- endfor %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{%- endblock %}
|
||||
|
19
maloja/web/jinja/startpage_modules/charts_albums.jinja
Normal file
19
maloja/web/jinja/startpage_modules/charts_albums.jinja
Normal file
@ -0,0 +1,19 @@
|
||||
<h1><a class="stat_link_topalbums" href="/charts_albums?in=alltime">Top Albums</a></h1>
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<span onclick="showStatsManual('topalbums','{{ r.identifier }}')" class="stat_selector_topalbums selector_topalbums_{{ r.identifier }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{{ "|" if not loop.last }}
|
||||
{%- endfor %}
|
||||
|
||||
<br/><br/>
|
||||
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<section class="stat_module_topalbums topalbums_{{ r.identifier }}" style="display:none;">
|
||||
{%- with limitkeys = {"timerange":r.range} -%}
|
||||
{% include 'partials/charts_albums_tiles.jinja' %}
|
||||
{%- endwith -%}
|
||||
</section>
|
||||
{%- endfor %}
|
19
maloja/web/jinja/startpage_modules/charts_artists.jinja
Normal file
19
maloja/web/jinja/startpage_modules/charts_artists.jinja
Normal file
@ -0,0 +1,19 @@
|
||||
<h1><a class="stat_link_topartists" href="/charts_artists?in=alltime">Top Artists</a></h1>
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<span onclick="showStatsManual('topartists','{{ r.identifier }}')" class="stat_selector_topartists selector_topartists_{{ r.identifier }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{{ "|" if not loop.last }}
|
||||
{%- endfor %}
|
||||
|
||||
<br/><br/>
|
||||
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<section class="stat_module_topartists topartists_{{ r.identifier }}" style="display:none;">
|
||||
{%- with limitkeys = {"timerange":r.range} -%}
|
||||
{% include 'partials/charts_artists_tiles.jinja' %}
|
||||
{%- endwith -%}
|
||||
</section>
|
||||
{%- endfor %}
|
19
maloja/web/jinja/startpage_modules/charts_tracks.jinja
Normal file
19
maloja/web/jinja/startpage_modules/charts_tracks.jinja
Normal file
@ -0,0 +1,19 @@
|
||||
<h1><a class="stat_link_toptracks" href="/charts_tracks?in=alltime">Top Tracks</a></h1>
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<span onclick="showStatsManual('toptracks','{{ r.identifier }}')" class="stat_selector_toptracks selector_toptracks_{{ r.identifier }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{{ "|" if not loop.last }}
|
||||
{%- endfor %}
|
||||
|
||||
<br/><br/>
|
||||
|
||||
|
||||
{% for r in xcurrent -%}
|
||||
<section class="stat_module_toptracks toptracks_{{ r.identifier }}" style="display:none;">
|
||||
{%- with limitkeys = {"timerange":r.range} -%}
|
||||
{% include 'partials/charts_tracks_tiles.jinja' %}
|
||||
{%- endwith -%}
|
||||
</section>
|
||||
{%- endfor %}
|
65
maloja/web/jinja/startpage_modules/featured.jinja
Normal file
65
maloja/web/jinja/startpage_modules/featured.jinja
Normal file
@ -0,0 +1,65 @@
|
||||
<!-- TODO: THIS IS PRELIMINARY. not sure if i want to keep this, but for now let's fill up that empty slot on the start page -->
|
||||
|
||||
<h1>Featured</h1>
|
||||
|
||||
{% set featured = dbc.get_featured() %}
|
||||
|
||||
{% set entitytypes = [
|
||||
{'identifier':'artist','localisation':"Artist", 'template':"info_artist.jinja", 'filterkeys':{"artist": featured.artist } },
|
||||
{'identifier':'track','localisation':"Track", 'template':"info_track.jinja", 'filterkeys':{"track": featured.track } },
|
||||
{'identifier':'album','localisation':"Album", 'template':"info_album.jinja", 'filterkeys':{"album": featured.album } }
|
||||
] %}
|
||||
|
||||
|
||||
{% for t in entitytypes -%}
|
||||
<span onclick="showStatsManual('featured','{{ t.identifier }}')" class="stat_selector_featured selector_featured_{{ t.identifier }}">
|
||||
{{ t.localisation }}
|
||||
</span>
|
||||
{{ "|" if not loop.last }}
|
||||
{%- endfor %}
|
||||
|
||||
<br/><br/><br/>
|
||||
|
||||
|
||||
{% for t in entitytypes -%}
|
||||
<section class="stat_module_featured featured_{{ t.identifier }}" style="display:none;">
|
||||
{%- with filterkeys = t.filterkeys -%}
|
||||
{%- with condensed = true -%}
|
||||
{% if filterkeys[t.identifier] %}
|
||||
{% include 'partials/' + t.template %}
|
||||
{% endif %}
|
||||
{%- endwith -%}
|
||||
{%- endwith -%}
|
||||
</section>
|
||||
{%- endfor %}
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
function showFeatured(t) {
|
||||
// Make all modules disappear
|
||||
var modules = document.getElementsByClassName("stat_module_featured");
|
||||
for (var i=0;i<modules.length;i++) {
|
||||
modules[i].setAttribute("style","display:none;");
|
||||
}
|
||||
|
||||
// Make requested module appear
|
||||
var reactivate = document.getElementsByClassName("featured_" + t);
|
||||
for (var i=0;i<reactivate.length;i++) {
|
||||
reactivate[i].setAttribute("style","");
|
||||
}
|
||||
|
||||
// Set all selectors to unselected
|
||||
var selectors = document.getElementsByClassName("stat_selector_featured");
|
||||
for (var i=0;i<selectors.length;i++) {
|
||||
selectors[i].setAttribute("style","");
|
||||
}
|
||||
|
||||
// Set the active selector to selected
|
||||
var reactivate = document.getElementsByClassName("stat_selector_featured_" + t);
|
||||
for (var i=0;i<reactivate.length;i++) {
|
||||
reactivate[i].setAttribute("style","opacity:0.5;");
|
||||
}
|
||||
}
|
||||
</script>
|
15
maloja/web/jinja/startpage_modules/lastscrobbles.jinja
Normal file
15
maloja/web/jinja/startpage_modules/lastscrobbles.jinja
Normal file
@ -0,0 +1,15 @@
|
||||
<h1><a href="/scrobbles">Last Scrobbles</a></h1>
|
||||
|
||||
{% for range in xcurrent %}
|
||||
<span class="stats">{{ range.localisation }}</span>
|
||||
<a href='/scrobbles?in={{ range.identifier }}'>{{ dbc.get_scrobbles_num({'timerange':range.range}) }}</a>
|
||||
{% endfor %}
|
||||
<br/><br/>
|
||||
|
||||
<span class="stat_module">
|
||||
|
||||
|
||||
{%- with amountkeys = {"perpage":12,"page":0}, shortTimeDesc=True -%}
|
||||
{% include 'partials/scrobbles.jinja' %}
|
||||
{%- endwith -%}
|
||||
</span>
|
17
maloja/web/jinja/startpage_modules/pulse.jinja
Normal file
17
maloja/web/jinja/startpage_modules/pulse.jinja
Normal file
@ -0,0 +1,17 @@
|
||||
<h1><a class="stat_link_pulse" href="/pulse?trail=1&step=month">Pulse</a></h1>
|
||||
|
||||
{% for range in xranges -%}
|
||||
<span onclick="showStatsManual('pulse','{{ range.identifier }}')" class="stat_selector_pulse selector_pulse_{{ range.identifier }}">
|
||||
{{ range.localisation }}
|
||||
</span>
|
||||
{{ "|" if not loop.last }}
|
||||
{%- endfor %}
|
||||
<br/><br/>
|
||||
|
||||
{% for range in xranges -%}
|
||||
<span class="stat_module_pulse pulse_{{ range.identifier }}" style="display:none;">
|
||||
{%- with limitkeys={"since":range.firstrange},delimitkeys={"step":range.identifier} -%}
|
||||
{% include 'partials/pulse.jinja' %}
|
||||
{%- endwith -%}
|
||||
</span>
|
||||
{%- endfor %}
|
31
maloja/web/jinja/top_albums.jinja
Normal file
31
maloja/web/jinja/top_albums.jinja
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - #1 Albums{% endblock %}
|
||||
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
<!-- find representative -->
|
||||
|
||||
{% set entries = dbc.get_top_albums(filterkeys,limitkeys,delimitkeys) %}
|
||||
{% set repr = entries | find_representative('album','scrobbles') %}
|
||||
{% set img = "/favicon.png" if repr is none else images.get_album_image(repr.album) %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('{{ img }}')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>#1 Albums</h1><br/>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
|
||||
<br/><br/>
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% include 'partials/top_albums.jinja' %}
|
||||
|
||||
{% endblock %}
|
@ -1,6 +1,7 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - #1 Artists{% endblock %}
|
||||
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
<!-- find representative -->
|
||||
|
||||
@ -17,10 +18,12 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>#1 Artists</h1><br/>
|
||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
|
||||
<br/><br/>
|
||||
{% with artistchart = True %}
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - #1 Tracks{% endblock %}
|
||||
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
<!-- find representative -->
|
||||
|
||||
@ -17,7 +18,7 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>#1 Tracks</h1><br/>
|
||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
|
||||
<br/><br/>
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
|
@ -4,11 +4,15 @@
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/rangeselect.js"></script>
|
||||
<script src="/edit.js"></script>
|
||||
<script src="/statselect.js"></script>
|
||||
<script>
|
||||
function scrobble(encodedtrack) {
|
||||
neo.xhttprequest('/apis/mlj_1/newscrobble?nofix&' + encodedtrack,data={},method="POST").then(response=>{window.location.reload()});
|
||||
function scrobbleThisTrack() {
|
||||
var payload = {
|
||||
"artists":{{ info.track.artists | tojson }},
|
||||
"title":{{ info.track.title | tojson }},
|
||||
"nofix": true
|
||||
}
|
||||
neo.xhttprequest("/apis/mlj_1/newscrobble",data=payload,method="POST",callback=notifyCallback,json=true).then(response=>{window.location.reload()});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -21,14 +25,26 @@
|
||||
|
||||
{% set encodedtrack = mlj_uri.uriencode({'track':track}) %}
|
||||
|
||||
{% block custombodyclasses %}
|
||||
{% if info.certification %}certified certified_{{ info.certification }}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block icon_bar %}
|
||||
{% if adminmode %}
|
||||
{% include 'icons/edit.jinja' %}
|
||||
|
||||
<div class="iconsubset mergeicons" data-entity_type="track" data-entity_id="{{ info.id }}" data-entity_name="{{ info.track.title }}">
|
||||
{% include 'icons/merge.jinja' %}
|
||||
{% include 'icons/merge_mark.jinja' %}
|
||||
{% include 'icons/merge_unmark.jinja' %}
|
||||
{% include 'icons/merge_cancel.jinja' %}
|
||||
<script>showValidMergeIcons();</script>
|
||||
</div>
|
||||
|
||||
<div class="iconsubset associateicons" data-entity_type="track" data-entity_id="{{ info.id }}" data-entity_name="{{ info.track.title }}">
|
||||
{% include 'icons/association_mark.jinja' %}
|
||||
{% include 'icons/association_unmark.jinja' %}
|
||||
{% include 'icons/association_cancel.jinja' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -44,43 +60,7 @@
|
||||
{% import 'partials/awards_track.jinja' as awards %}
|
||||
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
{% if adminmode %}
|
||||
<div
|
||||
class="changeable-image" data-uploader="b64=>upload('{{ encodedtrack }}',b64)"
|
||||
style="background-image:url('{{ images.get_track_image(track) }}');"
|
||||
title="Drag & Drop to upload new image"
|
||||
></div>
|
||||
{% else %}
|
||||
<div style="background-image:url('{{ images.get_track_image(track) }}');">
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text">
|
||||
<span>{{ links.links(track.artists) }}</span><br/>
|
||||
<h1 id="main_entity_name" class="headerwithextra">{{ info.track.title | e }}</h1>
|
||||
{{ awards.certs(track) }}
|
||||
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
|
||||
<br/>
|
||||
|
||||
<p class="stats">
|
||||
{% if adminmode %}<button type="button" onclick="scrobble('{{ encodedtrack }}')">Scrobble now</button>{% endif %}
|
||||
<a href="{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}">{{ info['scrobbles'] }} Scrobbles</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{ awards.medals(info) }}
|
||||
{{ awards.topweeks(info) }}
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% include 'partials/info_track.jinja' %}
|
||||
|
||||
|
||||
<table class="twopart">
|
||||
@ -91,8 +71,8 @@
|
||||
<br/>
|
||||
{% for r in xranges %}
|
||||
<span
|
||||
onclick="showRangeManual('pulse','{{ r.identifier }}')"
|
||||
class="stat_selector_pulse selector_pulse_{{ r.identifier }}"
|
||||
onclick="showStatsManual('pulseperformancecombined','{{ r.identifier }}')"
|
||||
class="stat_selector_pulseperformancecombined selector_pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'opacity:0.5;' if initialrange==r.identifier else '' }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
@ -104,7 +84,7 @@
|
||||
{% for r in xranges %}
|
||||
|
||||
<span
|
||||
class="stat_module_pulse pulse_{{ r.identifier }}"
|
||||
class="stat_module_pulseperformancecombined pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'display:none;' if initialrange!=r.identifier else '' }}"
|
||||
>
|
||||
|
||||
@ -121,8 +101,8 @@
|
||||
<br/>
|
||||
{% for r in xranges %}
|
||||
<span
|
||||
onclick="showRangeManual('pulse','{{ r.identifier }}')"
|
||||
class="stat_selector_pulse selector_pulse_{{ r.identifier }}"
|
||||
onclick="showStatsManual('pulseperformancecombined','{{ r.identifier }}')"
|
||||
class="stat_selector_pulseperformancecombined selector_pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'opacity:0.5;' if initialrange==r.identifier else '' }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
@ -134,7 +114,7 @@
|
||||
{% for r in xranges %}
|
||||
|
||||
<span
|
||||
class="stat_module_pulse pulse_{{ r.identifier }}"
|
||||
class="stat_module_pulseperformancecombined pulseperformancecombined_{{ r.identifier }}"
|
||||
style="{{ 'display:none;' if initialrange!=r.identifier else '' }}"
|
||||
>
|
||||
|
||||
@ -152,7 +132,7 @@
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}'>Last Scrobbles</a></h2>
|
||||
|
||||
{% with amountkeys = {"perpage":15,"page":0} %}
|
||||
{% with amountkeys = {"perpage":16,"page":0} %}
|
||||
{% include 'partials/scrobbles.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user