mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
Merge branch 'master' into fix/docker-image-size-180
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# TODO
|
||||
A list of things to do. Feel free to grab one of this and work on it.
|
||||
|
||||
## Clean up old playlist images
|
||||
When playlists are deleted or their images updated, the old
|
||||
images are not deleted.
|
||||
|
||||
Write a function to:
|
||||
- read all images in the playlist image directory and checks unlinked images by comparing the list with the .image property of each property
|
||||
- delete unlinked images
|
||||
|
||||
The function can run on app start up, before periodic checks
|
||||
|
||||
+3
-25
@@ -1,29 +1,7 @@
|
||||
# What's New?
|
||||
|
||||
- New opt-in alternate (no sidebar) layout
|
||||
- Added search bar to the top bar (all layouts)
|
||||
- Move browse options to homepage bottom
|
||||
- Move to a stronger WSGI server ([waitress](https://docs.pylonsproject.org/projects/waitress/en/stable/))
|
||||
- Proper ARM64 and other platforms support
|
||||
- A Proper timezone fix. Thanks to @tcsenpai on #170
|
||||
- Hovering over a recently played/added item on the homepage will reveal how long ago
|
||||
- Recently added items will not have a cutoff
|
||||
- The exhaustive list of web client stuff can be found on [commit 4211ccc](https://github.com/swing-opensource/swingmusic-client/commit/4211ccc685e3d33dbf008cbb6c77542baf0130dc) in the client repo.
|
||||
|
||||
# Bug fixes & Enhancements
|
||||
|
||||
- Lyrics plugin now works when Swing Music is auto started (tested on Ubuntu)
|
||||
- Track not being removed from queue
|
||||
- Playlist list page moving out of bounds
|
||||
- Save queue as playlist not working
|
||||
- Keyboard shortcuts not working in first try
|
||||
- Fix recently added items not filling row
|
||||
- Fix recently added items order
|
||||
- Hovering on recent favorite item will show how long ago it was ♥ed
|
||||
- Recently added playlist returns a max of 100 tracks, but without a cutoff period
|
||||
|
||||
# Development
|
||||
|
||||
- WIP code base documentation to `.github/docs`. Contributions are welcome!
|
||||
- Bump watchdog to v4
|
||||
|
||||
> [!TIP]
|
||||
> Plans for a mobile are underway. The development will be led by @EricGacoki
|
||||
- API documentation on /openapi
|
||||
@@ -79,7 +79,9 @@ yarn dev
|
||||
|
||||
You can see the client at http://localhost:5173.
|
||||
|
||||
> The client is hardcoded to hook into the server on port `1980` (to allow the another server instance to be running on the default port). You can follow the instructions above to set up the server in that port, or you can change the port in `swingmusic-client/config.ts`. Don't forget to change it back when in the PR.
|
||||
> The client is hardcoded to hook into the server on port `1980` (to allow the another server instance to be running on the default port).
|
||||
>
|
||||
> You can follow the instructions above to set up the server in that port, or you can change the port in `swingmusic-client/config.ts`. Don't forget to change it back when in the PR.
|
||||
|
||||
## Where can I go for help?
|
||||
|
||||
|
||||
@@ -62,15 +62,29 @@ jobs:
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.10.11"
|
||||
- name: Install Poetry
|
||||
- name: Create virtualenv
|
||||
run: |
|
||||
pip install poetry
|
||||
python -m venv .venv
|
||||
- name: Activate virtualenv (linux)
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
- name: Activate virtualenv (windows)
|
||||
if: matrix.os == 'windows-2019'
|
||||
run: |
|
||||
.venv\Scripts\Activate
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m poetry install
|
||||
pip install -r requirements.txt
|
||||
# - name: Install Poetry
|
||||
# run: |
|
||||
# pip install poetry
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m poetry install
|
||||
- name: Build server
|
||||
run: |
|
||||
python -m poetry run python manage.py --build
|
||||
python manage.py --build
|
||||
env:
|
||||
SWINGMUSIC_APP_VERSION: ${{ inputs.tag }}
|
||||
- name: Verify Linux build success
|
||||
@@ -169,6 +183,6 @@ jobs:
|
||||
tags: ghcr.io/${{github.repository}}:${{format('v{0}', inputs.tag)}}, ${{env.LATEST_TAG}}
|
||||
labels: org.opencontainers.image.title=Docker
|
||||
build-args: |
|
||||
client_tag=v${{inputs.tag}}
|
||||
app_version=${{inputs.tag}}
|
||||
env:
|
||||
LATEST_TAG: ${{ inputs.is_latest == 'true' && format('ghcr.io/{0}:latest', github.repository) || '' }}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
venv
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
|
||||
+10
-7
@@ -28,12 +28,15 @@ VOLUME /music
|
||||
|
||||
VOLUME /config
|
||||
|
||||
RUN python -m venv /venv
|
||||
|
||||
# RUN poetry config virtualenvs.create false
|
||||
RUN . /venv/bin/activate
|
||||
|
||||
# RUN poetry install
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
RUN apt-get update && apt-get install -y ffmpeg libavcodec-extra gcc-aarch64-linux-gnu
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
RUN poetry config virtualenvs.create false
|
||||
|
||||
RUN poetry install
|
||||
|
||||
ENTRYPOINT ["poetry", "run", "python", "manage.py", "--host", "0.0.0.0", "--config", "/config"]
|
||||
# ENTRYPOINT ["poetry", "run", "python", "manage.py", "--host", "0.0.0.0", "--config", "/config"]
|
||||
ENTRYPOINT ["python", "manage.py", "--host", "0.0.0.0", "--config", "/config"]
|
||||
|
||||
@@ -19,7 +19,15 @@ Swing Music is a beautiful, self-hosted music player for your local audio files.
|
||||
|
||||
Swing Music is available as pre-compiled binaries for Windows and Linux. Just download the latest release from the [downloads page](https://swingmusic.vercel.app/downloads) and launch it.
|
||||
|
||||
For Linux, you need to make the file executable first.
|
||||
[FFmpeg](https://ffmpeg.org/) is needed for the audio silence skip feature, so you need to install it first. On windows, you can follows [this tutorial](https://phoenixnap.com/kb/ffmpeg-windows) to install FFmpeg.
|
||||
|
||||
On Linux, you can install FFmpeg using:
|
||||
|
||||
```sh
|
||||
sudo apt-get install ffmpeg libavcodec-extra -y
|
||||
```
|
||||
|
||||
Then make the file executable first.
|
||||
|
||||
```bash
|
||||
chmod a+x ./swingmusic
|
||||
@@ -82,8 +90,7 @@ services:
|
||||
|
||||
Swing Music is looking for contributors. If you're interested, please join us at the [Swing Music Community](https://t.me/+9n61PFcgKhozZDE0) group on Telegram. For more information, take a look at https://github.com/swing-opensource/swingmusic/issues/186.
|
||||
|
||||
[Contributing Guidelines](.github/contributing.md).
|
||||
|
||||
[**CONTRIBUTING GUIDELINES**](.github/contributing.md).
|
||||
|
||||
### License
|
||||
|
||||
|
||||
+45
-19
@@ -2,10 +2,14 @@
|
||||
This module combines all API blueprints into a single Flask app instance.
|
||||
"""
|
||||
|
||||
from flask import Flask
|
||||
from flask_compress import Compress
|
||||
import datetime
|
||||
from flask_cors import CORS
|
||||
from flask_compress import Compress
|
||||
|
||||
from flask_openapi3 import Info
|
||||
from flask_openapi3 import OpenAPI
|
||||
|
||||
from app.settings import Keys
|
||||
from .plugins import lyrics as lyrics_plugin
|
||||
from app.api import (
|
||||
album,
|
||||
@@ -25,12 +29,34 @@ from app.api import (
|
||||
getall,
|
||||
)
|
||||
|
||||
# TODO: Move this description to a separate file
|
||||
open_api_description = f"""
|
||||
The REST API exposed by your Swing Music server
|
||||
|
||||
### Definition of terms:
|
||||
|
||||
#### 1. `limit`: The number of items to return.
|
||||
|
||||
In endpoints that request multiple lists of items, this represents the number of items to return for each list.
|
||||
|
||||
---
|
||||
|
||||
[MIT License](https://github.com/swing-opensource/swingmusic?tab=MIT-1-ov-file#MIT-1-ov-file) | Copyright (c) {datetime.datetime.now().year} [Mungai Njoroge](https://mungai.vercel.app)
|
||||
"""
|
||||
|
||||
|
||||
def create_api():
|
||||
"""
|
||||
Creates the Flask instance, registers modules and registers all the API blueprints.
|
||||
"""
|
||||
app = Flask(__name__)
|
||||
api_info = Info(
|
||||
title=f"Swing Music",
|
||||
version=f"v{Keys.SWINGMUSIC_APP_VERSION}",
|
||||
description=open_api_description,
|
||||
)
|
||||
|
||||
app = OpenAPI(__name__, info=api_info, doc_prefix="/docs")
|
||||
|
||||
CORS(app, origins="*")
|
||||
Compress(app)
|
||||
|
||||
@@ -39,29 +65,29 @@ def create_api():
|
||||
]
|
||||
|
||||
with app.app_context():
|
||||
app.register_blueprint(album.api)
|
||||
app.register_blueprint(artist.api)
|
||||
app.register_blueprint(send_file.api)
|
||||
app.register_blueprint(search.api)
|
||||
app.register_blueprint(folder.api)
|
||||
app.register_blueprint(playlist.api)
|
||||
app.register_blueprint(favorites.api)
|
||||
app.register_blueprint(imgserver.api)
|
||||
app.register_blueprint(settings.api)
|
||||
app.register_blueprint(colors.api)
|
||||
app.register_blueprint(lyrics.api)
|
||||
app.register_api(album.api)
|
||||
app.register_api(artist.api)
|
||||
app.register_api(send_file.api)
|
||||
app.register_api(search.api)
|
||||
app.register_api(folder.api)
|
||||
app.register_api(playlist.api)
|
||||
app.register_api(favorites.api)
|
||||
app.register_api(imgserver.api)
|
||||
app.register_api(settings.api)
|
||||
app.register_api(colors.api)
|
||||
app.register_api(lyrics.api)
|
||||
|
||||
# Plugins
|
||||
app.register_blueprint(plugins.api)
|
||||
app.register_blueprint(lyrics_plugin.api)
|
||||
app.register_api(plugins.api)
|
||||
app.register_api(lyrics_plugin.api)
|
||||
|
||||
# Logger
|
||||
app.register_blueprint(logger.api_bp)
|
||||
app.register_api(logger.api)
|
||||
|
||||
# Home
|
||||
app.register_blueprint(home.api_bp)
|
||||
app.register_api(home.api)
|
||||
|
||||
# Flask Restful
|
||||
app.register_blueprint(getall.api_bp)
|
||||
app.register_api(getall.api)
|
||||
|
||||
return app
|
||||
|
||||
+79
-62
@@ -4,41 +4,39 @@ Contains all the album routes.
|
||||
|
||||
import random
|
||||
|
||||
from flask import Blueprint, request
|
||||
from pydantic import Field
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from app.api.apischemas import AlbumHashSchema, AlbumLimitSchema, ArtistHashSchema
|
||||
|
||||
from app.db.sqlite.albumcolors import SQLiteAlbumMethods as adb
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as lastfmdb
|
||||
from app.lib.albumslib import sort_by_track_no
|
||||
from app.settings import Defaults
|
||||
from app.models import FavType, Track
|
||||
from app.serializers.album import serialize_for_card
|
||||
from app.serializers.track import serialize_track
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.tracks import TrackStore
|
||||
from app.utils.hashing import create_hash
|
||||
from app.lib.albumslib import sort_by_track_no
|
||||
from app.serializers.album import serialize_for_card
|
||||
from app.serializers.track import serialize_track
|
||||
from app.db.sqlite.albumcolors import SQLiteAlbumMethods as adb
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as lastfmdb
|
||||
|
||||
get_albums_by_albumartist = adb.get_albums_by_albumartist
|
||||
check_is_fav = favdb.check_is_favorite
|
||||
|
||||
api = Blueprint("album", __name__, url_prefix="")
|
||||
bp_tag = Tag(name="Album", description="Single album")
|
||||
api = APIBlueprint("album", __name__, url_prefix="/album", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
@api.route("/album", methods=["POST"])
|
||||
def get_album_tracks_and_info():
|
||||
# NOTE: Don't use "/" as it will cause redirects (failure)
|
||||
@api.post("")
|
||||
def get_album_tracks_and_info(body: AlbumHashSchema):
|
||||
"""
|
||||
Returns all the tracks in the given album
|
||||
Get album and tracks
|
||||
|
||||
Returns album info and tracks for the given albumhash.
|
||||
"""
|
||||
|
||||
data = request.get_json()
|
||||
error_msg = {"msg": "No hash provided"}
|
||||
|
||||
if data is None:
|
||||
return error_msg, 400
|
||||
|
||||
try:
|
||||
albumhash: str = data["albumhash"]
|
||||
except KeyError:
|
||||
return error_msg, 400
|
||||
albumhash = body.albumhash
|
||||
|
||||
error_msg = {"error": "Album not created yet."}
|
||||
album = AlbumStore.get_album_by_hash(albumhash)
|
||||
@@ -82,27 +80,43 @@ def get_album_tracks_and_info():
|
||||
}
|
||||
|
||||
|
||||
@api.route("/album/<albumhash>/tracks", methods=["GET"])
|
||||
def get_album_tracks(albumhash: str):
|
||||
@api.get("/<albumhash>/tracks")
|
||||
def get_album_tracks(path: AlbumHashSchema):
|
||||
"""
|
||||
Get album tracks
|
||||
|
||||
Returns all the tracks in the given album, sorted by disc and track number.
|
||||
NOTE: No album info is returned.
|
||||
"""
|
||||
tracks = TrackStore.get_tracks_by_albumhash(albumhash)
|
||||
tracks = TrackStore.get_tracks_by_albumhash(path.albumhash)
|
||||
tracks = sort_by_track_no(tracks)
|
||||
|
||||
return {"tracks": tracks}
|
||||
return tracks
|
||||
|
||||
|
||||
@api.route("/album/from-artist", methods=["POST"])
|
||||
def get_artist_albums():
|
||||
data = request.get_json()
|
||||
class GetMoreFromArtistsBody(AlbumLimitSchema):
|
||||
albumartists: str = Field(
|
||||
description="The artist hashes to get more albums from",
|
||||
example=Defaults.API_ARTISTHASH,
|
||||
)
|
||||
|
||||
if data is None:
|
||||
return {"msg": "No albumartist provided"}
|
||||
base_title: str = Field(
|
||||
description="The base title of the album to exclude from the results.",
|
||||
example=Defaults.API_ALBUMNAME,
|
||||
default=None,
|
||||
)
|
||||
|
||||
albumartists: str = data["albumartists"]
|
||||
limit: int = data.get("limit")
|
||||
base_title: str = data.get("base_title")
|
||||
|
||||
@api.post("/from-artist")
|
||||
def get_more_from_artist(body: GetMoreFromArtistsBody):
|
||||
"""
|
||||
Get more from artist
|
||||
|
||||
Returns more albums from the given artist hashes.
|
||||
"""
|
||||
albumartists = body.albumartists
|
||||
limit = body.limit
|
||||
base_title = body.base_title
|
||||
|
||||
albumartists: list[str] = albumartists.split(",")
|
||||
|
||||
@@ -125,23 +139,30 @@ def get_artist_albums():
|
||||
if len(a["albums"]) > 0
|
||||
]
|
||||
|
||||
return {"data": albums}
|
||||
return albums
|
||||
|
||||
|
||||
@api.route("/album/versions", methods=["POST"])
|
||||
def get_album_versions():
|
||||
class GetAlbumVersionsBody(ArtistHashSchema):
|
||||
og_album_title: str = Field(
|
||||
description="The original album title (album.og_title)",
|
||||
example=Defaults.API_ALBUMNAME,
|
||||
)
|
||||
base_title: str = Field(
|
||||
description="The base title of the album to exclude from the results.",
|
||||
example=Defaults.API_ALBUMNAME,
|
||||
)
|
||||
|
||||
|
||||
@api.post("/other-versions")
|
||||
def get_album_versions(body: GetAlbumVersionsBody):
|
||||
"""
|
||||
Get other versions
|
||||
|
||||
Returns other versions of the given album.
|
||||
"""
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return {"msg": "No albumartist provided"}
|
||||
|
||||
og_album_title: str = data["og_album_title"]
|
||||
base_title: str = data["base_title"]
|
||||
artisthash: str = data["artisthash"]
|
||||
og_album_title = body.og_album_title
|
||||
base_title = body.base_title
|
||||
artisthash = body.artisthash
|
||||
|
||||
albums = AlbumStore.get_albums_by_artisthash(artisthash)
|
||||
|
||||
@@ -156,26 +177,22 @@ def get_album_versions():
|
||||
tracks = TrackStore.get_tracks_by_albumhash(a.albumhash)
|
||||
a.get_date_from_tracks(tracks)
|
||||
|
||||
return {"data": albums}
|
||||
return albums
|
||||
|
||||
|
||||
@api.route("/album/similar", methods=["GET"])
|
||||
def get_similar_albums():
|
||||
class GetSimilarAlbumsQuery(ArtistHashSchema, AlbumLimitSchema):
|
||||
pass
|
||||
|
||||
|
||||
@api.get("/similar")
|
||||
def get_similar_albums(query: GetSimilarAlbumsQuery):
|
||||
"""
|
||||
Get similar albums
|
||||
|
||||
Returns similar albums to the given album.
|
||||
"""
|
||||
data = request.args
|
||||
|
||||
if data is None:
|
||||
return {"msg": "No artisthash provided"}
|
||||
|
||||
artisthash: str = data["artisthash"]
|
||||
limit: int = data.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
artisthash = query.artisthash
|
||||
limit = query.limit
|
||||
|
||||
similar_artists = lastfmdb.get_similar_artists_for(artisthash)
|
||||
|
||||
@@ -197,4 +214,4 @@ def get_similar_albums():
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {"albums": [serialize_for_card(a) for a in albums[:limit]]}
|
||||
return [serialize_for_card(a) for a in albums[:limit]]
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Reusable Pydantic basic schemas for the API
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.settings import Defaults
|
||||
|
||||
|
||||
class AlbumHashSchema(BaseModel):
|
||||
"""
|
||||
Extending this class will give you a model with the `albumhash` field
|
||||
"""
|
||||
|
||||
albumhash: str = Field(
|
||||
description="The album hash",
|
||||
example=Defaults.API_ALBUMHASH,
|
||||
min_length=Defaults.HASH_LENGTH,
|
||||
max_length=Defaults.HASH_LENGTH,
|
||||
)
|
||||
|
||||
|
||||
class ArtistHashSchema(BaseModel):
|
||||
"""
|
||||
Extending this class will give you a model with the `artisthash` field
|
||||
"""
|
||||
|
||||
artisthash: str = Field(
|
||||
description="The artist hash",
|
||||
example=Defaults.API_ARTISTHASH,
|
||||
min_length=Defaults.HASH_LENGTH,
|
||||
max_length=Defaults.HASH_LENGTH,
|
||||
)
|
||||
|
||||
|
||||
class TrackHashSchema(BaseModel):
|
||||
"""
|
||||
Extending this class will give you a model with the `trackhash` field
|
||||
"""
|
||||
|
||||
trackhash: str = Field(
|
||||
description="The track hash",
|
||||
example=Defaults.API_TRACKHASH,
|
||||
min_length=Defaults.HASH_LENGTH,
|
||||
max_length=Defaults.HASH_LENGTH,
|
||||
)
|
||||
|
||||
|
||||
class GenericLimitSchema(BaseModel):
|
||||
"""
|
||||
Extending this class will give you a model with the `limit` field
|
||||
"""
|
||||
|
||||
limit: int = Field(
|
||||
description="The number of items to return",
|
||||
example=Defaults.API_CARD_LIMIT,
|
||||
default=Defaults.API_CARD_LIMIT,
|
||||
)
|
||||
|
||||
|
||||
# INFO: The following 3 classes are duplicated to specify the type of items
|
||||
class TrackLimitSchema(BaseModel):
|
||||
"""
|
||||
Extending this class will give you a model with the `limit` field
|
||||
"""
|
||||
|
||||
limit: int = Field(
|
||||
description="The number of tracks to return",
|
||||
example=Defaults.API_CARD_LIMIT,
|
||||
default=Defaults.API_CARD_LIMIT,
|
||||
)
|
||||
|
||||
|
||||
class AlbumLimitSchema(BaseModel):
|
||||
"""
|
||||
Extending this class will give you a model with the `limit` field
|
||||
"""
|
||||
|
||||
limit: int = Field(
|
||||
description="The number of albums to return",
|
||||
example=Defaults.API_CARD_LIMIT,
|
||||
default=Defaults.API_CARD_LIMIT,
|
||||
)
|
||||
|
||||
|
||||
class ArtistLimitSchema(BaseModel):
|
||||
"""
|
||||
Extending this class will give you a model with the `limit` field
|
||||
"""
|
||||
|
||||
limit: int = Field(
|
||||
description="The number of artists to return",
|
||||
example=Defaults.API_CARD_LIMIT,
|
||||
default=Defaults.API_CARD_LIMIT,
|
||||
)
|
||||
+38
-35
@@ -1,11 +1,14 @@
|
||||
"""
|
||||
Contains all the artist(s) routes.
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask_openapi3 import APIBlueprint, Tag
|
||||
from pydantic import Field
|
||||
from app.api.apischemas import AlbumLimitSchema, ArtistHashSchema, ArtistLimitSchema, TrackLimitSchema
|
||||
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as fmdb
|
||||
@@ -16,20 +19,19 @@ from app.store.albums import AlbumStore
|
||||
from app.store.artists import ArtistStore
|
||||
from app.store.tracks import TrackStore
|
||||
|
||||
api = Blueprint("artist", __name__, url_prefix="/")
|
||||
bp_tag = Tag(name="Artist", description="Single artist")
|
||||
api = APIBlueprint("artist", __name__, url_prefix="/artist", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
@api.route("/artist/<artisthash>", methods=["GET"])
|
||||
def get_artist(artisthash: str):
|
||||
@api.get("/<string:artisthash>")
|
||||
def get_artist(path: ArtistHashSchema, query: TrackLimitSchema):
|
||||
"""
|
||||
Get artist data.
|
||||
Get artist
|
||||
|
||||
Returns artist data, tracks and genres for the given artisthash.
|
||||
"""
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
artisthash = path.artisthash
|
||||
limit = query.limit
|
||||
|
||||
artist = ArtistStore.get_artist_by_hash(artisthash)
|
||||
|
||||
@@ -79,19 +81,23 @@ def get_artist(artisthash: str):
|
||||
}
|
||||
|
||||
|
||||
@api.route("/artist/<artisthash>/albums", methods=["GET"])
|
||||
def get_artist_albums(artisthash: str):
|
||||
limit = request.args.get("limit")
|
||||
class GetArtistAlbumsQuery(AlbumLimitSchema):
|
||||
all: bool = Field(
|
||||
description="Whether to ignore limit and return all albums", default=False
|
||||
)
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
return_all = request.args.get("all")
|
||||
@api.get("/<artisthash>/albums")
|
||||
def get_artist_albums(path: ArtistHashSchema, query: GetArtistAlbumsQuery):
|
||||
"""
|
||||
Get artist albums.
|
||||
"""
|
||||
return_all = query.all
|
||||
artisthash = path.artisthash
|
||||
|
||||
limit = int(limit)
|
||||
limit = query.limit
|
||||
|
||||
all_albums = AlbumStore.get_albums_by_artisthash(artisthash)
|
||||
|
||||
# start: check for missing albums. ie. compilations and features
|
||||
all_tracks = TrackStore.get_tracks_by_artisthash(artisthash)
|
||||
|
||||
@@ -156,7 +162,7 @@ def get_artist_albums(artisthash: str):
|
||||
if artist is None:
|
||||
return {"error": "Artist not found"}, 404
|
||||
|
||||
if return_all is not None and return_all == "true":
|
||||
if return_all:
|
||||
limit = len(all_albums)
|
||||
|
||||
singles_and_eps = singles + eps
|
||||
@@ -170,29 +176,26 @@ def get_artist_albums(artisthash: str):
|
||||
}
|
||||
|
||||
|
||||
@api.route("/artist/<artisthash>/tracks", methods=["GET"])
|
||||
def get_all_artist_tracks(artisthash: str):
|
||||
@api.get("/<artisthash>/tracks")
|
||||
def get_all_artist_tracks(path: ArtistHashSchema):
|
||||
"""
|
||||
Get artist tracks
|
||||
|
||||
Returns all artists by a given artist.
|
||||
"""
|
||||
tracks = TrackStore.get_tracks_by_artisthash(artisthash)
|
||||
tracks = TrackStore.get_tracks_by_artisthash(path.artisthash)
|
||||
|
||||
return {"tracks": serialize_tracks(tracks)}
|
||||
return serialize_tracks(tracks)
|
||||
|
||||
|
||||
@api.route("/artist/<artisthash>/similar", methods=["GET"])
|
||||
def get_similar_artists(artisthash: str):
|
||||
@api.get("/<artisthash>/similar")
|
||||
def get_similar_artists(path: ArtistHashSchema, query: ArtistLimitSchema):
|
||||
"""
|
||||
Returns similar artists.
|
||||
Get similar artists.
|
||||
"""
|
||||
limit = request.args.get("limit")
|
||||
limit = query.limit
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
artist = ArtistStore.get_artist_by_hash(artisthash)
|
||||
artist = ArtistStore.get_artist_by_hash(path.artisthash)
|
||||
|
||||
if artist is None:
|
||||
return {"error": "Artist not found"}, 404
|
||||
@@ -207,7 +210,7 @@ def get_similar_artists(artisthash: str):
|
||||
if len(similar) > limit:
|
||||
similar = random.sample(similar, limit)
|
||||
|
||||
return {"artists": similar[:limit]}
|
||||
return similar[:limit]
|
||||
|
||||
|
||||
# TODO: Rewrite this file using generators where possible
|
||||
|
||||
+11
-5
@@ -1,12 +1,18 @@
|
||||
from flask import Blueprint
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from app.api.apischemas import AlbumHashSchema
|
||||
from app.store.albums import AlbumStore as Store
|
||||
|
||||
api = Blueprint("colors", __name__, url_prefix="/colors")
|
||||
bp_tag = Tag(name="Colors", description="Get item colors")
|
||||
api = APIBlueprint("colors", __name__, url_prefix="/colors", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
@api.route("/album/<albumhash>")
|
||||
def get_album_color(albumhash: str):
|
||||
album = Store.get_album_by_hash(albumhash)
|
||||
@api.get("/album/<albumhash>")
|
||||
def get_album_color(path: AlbumHashSchema):
|
||||
"""
|
||||
Get album color
|
||||
"""
|
||||
album = Store.get_album_by_hash(path.albumhash)
|
||||
|
||||
msg = {"color": ""}
|
||||
|
||||
|
||||
+98
-88
@@ -1,36 +1,51 @@
|
||||
from flask import Blueprint, request
|
||||
from typing import List, TypeVar
|
||||
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.api.apischemas import GenericLimitSchema
|
||||
from app.models import FavType
|
||||
from app.serializers.album import serialize_for_card, serialize_for_card_many
|
||||
from app.serializers.artist import serialize_for_card as serialize_artist
|
||||
from app.settings import Defaults
|
||||
from app.utils.bisection import use_bisection
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.serializers.track import serialize_track, serialize_tracks
|
||||
from app.utils.bisection import UseBisection
|
||||
from app.serializers.artist import serialize_for_card as serialize_artist
|
||||
from app.serializers.album import serialize_for_card, serialize_for_card_many
|
||||
|
||||
from app.store.artists import ArtistStore
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.tracks import TrackStore
|
||||
from app.store.artists import ArtistStore
|
||||
from app.utils.dates import timestamp_to_time_passed
|
||||
|
||||
bp_tag = Tag(name="Favorites", description="Your favorite items")
|
||||
api = APIBlueprint("favorites", __name__, url_prefix="/favorites", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
api = Blueprint("favorite", __name__, url_prefix="/")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def remove_none(items: list):
|
||||
def remove_none(items: List[T]) -> List[T]:
|
||||
return [i for i in items if i is not None]
|
||||
|
||||
|
||||
@api.route("/favorite/add", methods=["POST"])
|
||||
def add_favorite():
|
||||
class FavoritesAddBody(BaseModel):
|
||||
hash: str = Field(
|
||||
description="The hash of the item",
|
||||
min_length=Defaults.HASH_LENGTH,
|
||||
max_length=Defaults.HASH_LENGTH,
|
||||
example=Defaults.API_ALBUMHASH,
|
||||
)
|
||||
type: str = Field(description="The type of the item", example=FavType.album)
|
||||
|
||||
|
||||
@api.post("/add")
|
||||
def add_favorite(body: FavoritesAddBody):
|
||||
"""
|
||||
Adds a favorite to the database.
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return {"error": "No data provided"}, 400
|
||||
|
||||
itemhash = data.get("hash")
|
||||
itemtype = data.get("type")
|
||||
itemhash = body.hash
|
||||
itemtype = body.type
|
||||
|
||||
favdb.insert_one_favorite(itemtype, itemhash)
|
||||
|
||||
@@ -40,18 +55,13 @@ def add_favorite():
|
||||
return {"msg": "Added to favorites"}
|
||||
|
||||
|
||||
@api.route("/favorite/remove", methods=["POST"])
|
||||
def remove_favorite():
|
||||
@api.post("/remove")
|
||||
def remove_favorite(body: FavoritesAddBody):
|
||||
"""
|
||||
Removes a favorite from the database.
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return {"error": "No data provided"}, 400
|
||||
|
||||
itemhash = data.get("hash")
|
||||
itemtype = data.get("type")
|
||||
itemhash = body.hash
|
||||
itemtype = body.type
|
||||
|
||||
favdb.delete_favorite(itemtype, itemhash)
|
||||
|
||||
@@ -61,22 +71,19 @@ def remove_favorite():
|
||||
return {"msg": "Removed from favorites"}
|
||||
|
||||
|
||||
@api.route("/albums/favorite")
|
||||
def get_favorite_albums():
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
@api.get("/albums")
|
||||
def get_favorite_albums(query: GenericLimitSchema):
|
||||
"""
|
||||
Get favorite albums
|
||||
"""
|
||||
limit = query.limit
|
||||
albums = favdb.get_fav_albums()
|
||||
albumhashes = [a[1] for a in albums]
|
||||
albumhashes.reverse()
|
||||
|
||||
src_albums = sorted(AlbumStore.albums, key=lambda x: x.albumhash)
|
||||
|
||||
fav_albums = UseBisection(src_albums, "albumhash", albumhashes)()
|
||||
fav_albums = use_bisection(src_albums, "albumhash", albumhashes)
|
||||
fav_albums = remove_none(fav_albums)
|
||||
|
||||
if limit == 0:
|
||||
@@ -85,21 +92,18 @@ def get_favorite_albums():
|
||||
return {"albums": serialize_for_card_many(fav_albums[:limit])}
|
||||
|
||||
|
||||
@api.route("/tracks/favorite")
|
||||
def get_favorite_tracks():
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
@api.get("/tracks")
|
||||
def get_favorite_tracks(query: GenericLimitSchema):
|
||||
"""
|
||||
Get favorite tracks
|
||||
"""
|
||||
limit = query.limit
|
||||
tracks = favdb.get_fav_tracks()
|
||||
trackhashes = [t[1] for t in tracks]
|
||||
trackhashes.reverse()
|
||||
src_tracks = sorted(TrackStore.tracks, key=lambda x: x.trackhash)
|
||||
|
||||
tracks = UseBisection(src_tracks, "trackhash", trackhashes)()
|
||||
tracks = use_bisection(src_tracks, "trackhash", trackhashes)
|
||||
tracks = remove_none(tracks)
|
||||
|
||||
if limit == 0:
|
||||
@@ -108,22 +112,19 @@ def get_favorite_tracks():
|
||||
return {"tracks": serialize_tracks(tracks[:limit])}
|
||||
|
||||
|
||||
@api.route("/artists/favorite")
|
||||
def get_favorite_artists():
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
@api.get("/artists")
|
||||
def get_favorite_artists(query: GenericLimitSchema):
|
||||
"""
|
||||
Get favorite artists
|
||||
"""
|
||||
limit = query.limit
|
||||
artists = favdb.get_fav_artists()
|
||||
artisthashes = [a[1] for a in artists]
|
||||
artisthashes.reverse()
|
||||
|
||||
src_artists = sorted(ArtistStore.artists, key=lambda x: x.artisthash)
|
||||
|
||||
artists = UseBisection(src_artists, "artisthash", artisthashes)()
|
||||
artists = use_bisection(src_artists, "artisthash", artisthashes)
|
||||
artists = remove_none(artists)
|
||||
|
||||
if limit == 0:
|
||||
@@ -132,27 +133,38 @@ def get_favorite_artists():
|
||||
return {"artists": artists[:limit]}
|
||||
|
||||
|
||||
@api.route("/favorites")
|
||||
def get_all_favorites():
|
||||
class GetAllFavoritesQuery(BaseModel):
|
||||
"""
|
||||
Extending this class will give you a model with the `limit` field
|
||||
"""
|
||||
|
||||
track_limit: int = Field(
|
||||
description="The number of tracks to return",
|
||||
example=Defaults.API_CARD_LIMIT,
|
||||
default=Defaults.API_CARD_LIMIT,
|
||||
)
|
||||
|
||||
album_limit: int = Field(
|
||||
description="The number of albums to return",
|
||||
example=Defaults.API_CARD_LIMIT,
|
||||
default=Defaults.API_CARD_LIMIT,
|
||||
)
|
||||
|
||||
artist_limit: int = Field(
|
||||
description="The number of artists to return",
|
||||
example=Defaults.API_CARD_LIMIT,
|
||||
default=Defaults.API_CARD_LIMIT,
|
||||
)
|
||||
|
||||
|
||||
@api.get("")
|
||||
def get_all_favorites(query: GetAllFavoritesQuery):
|
||||
"""
|
||||
Returns all the favorites in the database.
|
||||
"""
|
||||
track_limit = request.args.get("track_limit")
|
||||
album_limit = request.args.get("album_limit")
|
||||
artist_limit = request.args.get("artist_limit")
|
||||
|
||||
if track_limit is None:
|
||||
track_limit = 6
|
||||
|
||||
if album_limit is None:
|
||||
album_limit = 6
|
||||
|
||||
if artist_limit is None:
|
||||
artist_limit = 6
|
||||
|
||||
track_limit = int(track_limit)
|
||||
album_limit = int(album_limit)
|
||||
artist_limit = int(artist_limit)
|
||||
track_limit = query.track_limit
|
||||
album_limit = query.album_limit
|
||||
artist_limit = query.artist_limit
|
||||
|
||||
# largest is x2 to accound for broken hashes if any
|
||||
largest = max(track_limit, album_limit, artist_limit)
|
||||
@@ -169,6 +181,7 @@ def get_all_favorites():
|
||||
artist_master_hash = set(a.artisthash for a in ArtistStore.artists)
|
||||
|
||||
for fav in favs:
|
||||
# INFO: hash is [1], type is [2], timestamp is [3]
|
||||
hash = fav[1]
|
||||
if fav[2] == FavType.track:
|
||||
tracks.append(hash) if hash in track_master_hash else None
|
||||
@@ -189,9 +202,9 @@ def get_all_favorites():
|
||||
src_albums = sorted(AlbumStore.albums, key=lambda x: x.albumhash)
|
||||
src_artists = sorted(ArtistStore.artists, key=lambda x: x.artisthash)
|
||||
|
||||
tracks = UseBisection(src_tracks, "trackhash", tracks, limit=track_limit)()
|
||||
albums = UseBisection(src_albums, "albumhash", albums, limit=album_limit)()
|
||||
artists = UseBisection(src_artists, "artisthash", artists, limit=artist_limit)()
|
||||
tracks = use_bisection(src_tracks, "trackhash", tracks, limit=track_limit)
|
||||
albums = use_bisection(src_albums, "albumhash", albums, limit=album_limit)
|
||||
artists = use_bisection(src_artists, "artisthash", artists, limit=artist_limit)
|
||||
|
||||
tracks = remove_none(tracks)
|
||||
albums = remove_none(albums)
|
||||
@@ -201,6 +214,7 @@ def get_all_favorites():
|
||||
# first_n = favs
|
||||
|
||||
for fav in favs:
|
||||
# INFO: hash is [1], type is [2], timestamp is [3]
|
||||
if len(recents) >= largest:
|
||||
break
|
||||
|
||||
@@ -212,6 +226,7 @@ def get_all_favorites():
|
||||
|
||||
album = serialize_for_card(album)
|
||||
album["help_text"] = "album"
|
||||
album["time"] = timestamp_to_time_passed(fav[3])
|
||||
|
||||
recents.append(
|
||||
{
|
||||
@@ -228,6 +243,7 @@ def get_all_favorites():
|
||||
|
||||
artist = serialize_artist(artist)
|
||||
artist["help_text"] = "artist"
|
||||
artist["time"] = timestamp_to_time_passed(fav[3])
|
||||
|
||||
recents.append(
|
||||
{
|
||||
@@ -244,6 +260,7 @@ def get_all_favorites():
|
||||
|
||||
track = serialize_track(track)
|
||||
track["help_text"] = "track"
|
||||
track["time"] = timestamp_to_time_passed(fav[3])
|
||||
|
||||
recents.append({"type": "track", "item": track})
|
||||
|
||||
@@ -256,20 +273,13 @@ def get_all_favorites():
|
||||
}
|
||||
|
||||
|
||||
@api.route("/favorites/check")
|
||||
def check_favorite():
|
||||
@api.get("/check")
|
||||
def check_favorite(query: FavoritesAddBody):
|
||||
"""
|
||||
Checks if a favorite exists in the database.
|
||||
"""
|
||||
itemhash = request.args.get("hash")
|
||||
itemtype = request.args.get("type")
|
||||
|
||||
if itemhash is None:
|
||||
return {"error": "No hash provided"}, 400
|
||||
|
||||
if itemtype is None:
|
||||
return {"error": "No type provided"}, 400
|
||||
|
||||
itemhash = query.hash
|
||||
itemtype = query.type
|
||||
exists = favdb.check_is_favorite(itemhash, itemtype)
|
||||
|
||||
return {"is_favorite": exists}
|
||||
|
||||
+62
-37
@@ -1,11 +1,14 @@
|
||||
"""
|
||||
Contains all the folder routes.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
from flask import Blueprint, request
|
||||
from pydantic import BaseModel, Field
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from showinfm import show_in_file_manager
|
||||
|
||||
from app import settings
|
||||
@@ -15,24 +18,24 @@ from app.serializers.track import serialize_track
|
||||
from app.store.tracks import TrackStore as store
|
||||
from app.utils.wintools import is_windows, win_replace_slash
|
||||
|
||||
api = Blueprint("folder", __name__, url_prefix="")
|
||||
tag = Tag(name="Folders", description="Get folders and tracks in a directory")
|
||||
api = APIBlueprint("folder", __name__, url_prefix="/folder", abp_tags=[tag])
|
||||
|
||||
|
||||
@api.route("/folder", methods=["POST"])
|
||||
def get_folder_tree():
|
||||
class FolderTree(BaseModel):
|
||||
folder: str = Field("$home", description="The folder to things from")
|
||||
tracks_only: bool = Field(False, description="Whether to only get tracks")
|
||||
|
||||
|
||||
@api.post("")
|
||||
def get_folder_tree(body: FolderTree):
|
||||
"""
|
||||
Get folder
|
||||
|
||||
Returns a list of all the folders and tracks in the given folder.
|
||||
"""
|
||||
data = request.get_json()
|
||||
req_dir = "$home"
|
||||
|
||||
tracks_only = False
|
||||
if data is not None:
|
||||
try:
|
||||
req_dir: str = data["folder"]
|
||||
tracks_only: bool = data["tracks_only"]
|
||||
except KeyError:
|
||||
req_dir = "$home"
|
||||
req_dir = body.folder
|
||||
tracks_only = body.tracks_only
|
||||
|
||||
root_dirs = db.get_root_dirs()
|
||||
root_dirs.sort()
|
||||
@@ -44,6 +47,9 @@ def get_folder_tree():
|
||||
pass
|
||||
|
||||
if req_dir == "$home":
|
||||
if len(root_dirs) == 1:
|
||||
req_dir = root_dirs[0]
|
||||
else:
|
||||
folders = get_folders(root_dirs)
|
||||
|
||||
return {
|
||||
@@ -92,18 +98,23 @@ def get_all_drives(is_win: bool = False):
|
||||
return drives
|
||||
|
||||
|
||||
@api.route("/folder/dir-browser", methods=["POST"])
|
||||
def list_folders():
|
||||
"""
|
||||
Returns a list of all the folders in the given folder.
|
||||
"""
|
||||
data = request.get_json()
|
||||
is_win = is_windows()
|
||||
class DirBrowserBody(BaseModel):
|
||||
folder: str = Field(
|
||||
"$root",
|
||||
description="The folder to list directories from",
|
||||
)
|
||||
|
||||
try:
|
||||
req_dir: str = data["folder"]
|
||||
except KeyError:
|
||||
req_dir = "$root"
|
||||
|
||||
@api.post("/dir-browser")
|
||||
def list_folders(body: DirBrowserBody):
|
||||
"""
|
||||
List folders
|
||||
|
||||
Returns a list of all the folders in the given folder.
|
||||
Used when selecting root dirs.
|
||||
"""
|
||||
req_dir = body.folder
|
||||
is_win = is_windows()
|
||||
|
||||
if req_dir == "$root":
|
||||
return {
|
||||
@@ -131,26 +142,40 @@ def list_folders():
|
||||
}
|
||||
|
||||
|
||||
@api.route("/folder/show-in-files")
|
||||
def open_in_file_manager():
|
||||
path = request.args.get("path")
|
||||
class FolderOpenInFileManagerQuery(BaseModel):
|
||||
path: str = Field(
|
||||
description="The path to open in the file manager",
|
||||
)
|
||||
|
||||
if path is None:
|
||||
return {"error": "No path provided."}, 400
|
||||
|
||||
show_in_file_manager(path)
|
||||
@api.get("/show-in-files")
|
||||
def open_in_file_manager(query: FolderOpenInFileManagerQuery):
|
||||
"""
|
||||
Open in file manager
|
||||
|
||||
Opens the given path in the file manager on the host machine.
|
||||
"""
|
||||
show_in_file_manager(query.path)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@api.route("/folder/tracks/all")
|
||||
def get_tracks_in_path():
|
||||
path = request.args.get("path")
|
||||
class GetTracksInPathQuery(BaseModel):
|
||||
path: str = Field(
|
||||
description="The path to get tracks from",
|
||||
)
|
||||
|
||||
if path is None:
|
||||
return {"error": "No path provided."}, 400
|
||||
|
||||
tracks = store.get_tracks_in_path(path)
|
||||
@api.get("/tracks/all")
|
||||
def get_tracks_in_path(query: GetTracksInPathQuery):
|
||||
"""
|
||||
Get tracks in path
|
||||
|
||||
Gets all (or a max of 300) tracks from the given path and its subdirectories.
|
||||
|
||||
Used when adding tracks to the queue.
|
||||
"""
|
||||
tracks = store.get_tracks_in_path(query.path)
|
||||
tracks = sorted(tracks, key=lambda i: i.last_mod)
|
||||
tracks = (serialize_track(t) for t in tracks if Path(t.filepath).exists())
|
||||
|
||||
|
||||
+120
-5
@@ -1,10 +1,125 @@
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .resources import Albums
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
api_bp = Blueprint("getall", __name__, url_prefix="/getall")
|
||||
api = Api(api_bp)
|
||||
from datetime import datetime
|
||||
from app.api.apischemas import GenericLimitSchema
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.artists import ArtistStore
|
||||
|
||||
from app.serializers.album import serialize_for_card as serialize_album
|
||||
from app.serializers.artist import serialize_for_card as serialize_artist
|
||||
from app.utils import format_number
|
||||
from app.utils.dates import (
|
||||
create_new_date,
|
||||
date_string_to_time_passed,
|
||||
seconds_to_time_string,
|
||||
)
|
||||
|
||||
bp_tag = Tag(name="Get all", description="List all items")
|
||||
api = APIBlueprint("getall", __name__, url_prefix="/getall", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
api.add_resource(Albums, "/<itemtype>")
|
||||
class GetAllItemsBody(GenericLimitSchema):
|
||||
start: int = Field(
|
||||
description="The start index of the items to return",
|
||||
example=0,
|
||||
default=0,
|
||||
)
|
||||
sortby: str = Field(
|
||||
description="The key to sort items by",
|
||||
example="created_date",
|
||||
default="created_date",
|
||||
)
|
||||
|
||||
reverse: int = Field(
|
||||
description="Reverse the sort",
|
||||
example=1,
|
||||
default=1,
|
||||
)
|
||||
|
||||
|
||||
class GetAllItemsPath(BaseModel):
|
||||
itemtype: str = Field(
|
||||
description="The type of items to return (albums | artists)",
|
||||
example="albums",
|
||||
default="albums",
|
||||
)
|
||||
|
||||
|
||||
@api.get("/<itemtype>")
|
||||
def get_all_items(path: GetAllItemsPath, query: GetAllItemsBody):
|
||||
"""
|
||||
Get all items
|
||||
|
||||
Used to show all albums or artists in the library
|
||||
"""
|
||||
is_albums = path.itemtype == "albums"
|
||||
is_artists = path.itemtype == "artists"
|
||||
|
||||
items = AlbumStore.albums
|
||||
|
||||
if is_artists:
|
||||
items = ArtistStore.artists
|
||||
|
||||
start = query.start
|
||||
limit = query.limit
|
||||
sort = query.sortby
|
||||
reverse = query.reverse == 1
|
||||
|
||||
# if sort == "":
|
||||
# sort = "created_date"
|
||||
|
||||
sort_is_count = sort == "count"
|
||||
sort_is_duration = sort == "duration"
|
||||
sort_is_create_date = sort == "created_date"
|
||||
|
||||
sort_is_date = is_albums and sort == "date"
|
||||
sort_is_artist = is_albums and sort == "albumartists"
|
||||
|
||||
sort_is_artist_trackcount = is_artists and sort == "trackcount"
|
||||
sort_is_artist_albumcount = is_artists and sort == "albumcount"
|
||||
|
||||
lambda_sort = lambda x: getattr(x, sort)
|
||||
if sort_is_artist:
|
||||
lambda_sort = lambda x: getattr(x, sort)[0].name
|
||||
|
||||
sorted_items = sorted(items, key=lambda_sort, reverse=reverse)
|
||||
items = sorted_items[start : start + limit]
|
||||
|
||||
album_list = []
|
||||
|
||||
for item in items:
|
||||
item_dict = serialize_album(item) if is_albums else serialize_artist(item)
|
||||
|
||||
if sort_is_date:
|
||||
item_dict["help_text"] = item.date
|
||||
|
||||
if sort_is_create_date:
|
||||
date = create_new_date(datetime.fromtimestamp(item.created_date))
|
||||
timeago = date_string_to_time_passed(date)
|
||||
item_dict["help_text"] = timeago
|
||||
|
||||
if sort_is_count:
|
||||
item_dict["help_text"] = (
|
||||
f"{format_number(item.count)} track{'' if item.count == 1 else 's'}"
|
||||
)
|
||||
|
||||
if sort_is_duration:
|
||||
item_dict["help_text"] = seconds_to_time_string(item.duration)
|
||||
|
||||
if sort_is_artist_trackcount:
|
||||
item_dict["help_text"] = (
|
||||
f"{format_number(item.trackcount)} track{'' if item.trackcount == 1 else 's'}"
|
||||
)
|
||||
|
||||
if sort_is_artist_albumcount:
|
||||
item_dict["help_text"] = (
|
||||
f"{format_number(item.albumcount)} album{'' if item.albumcount == 1 else 's'}"
|
||||
)
|
||||
|
||||
album_list.append(item_dict)
|
||||
|
||||
return {"items": album_list, "total": len(sorted_items)}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
from flask_restful import Resource, reqparse
|
||||
from datetime import datetime
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.artists import ArtistStore
|
||||
|
||||
from app.serializers.album import serialize_for_card as serialize_album
|
||||
from app.serializers.artist import serialize_for_card as serialize_artist
|
||||
from app.utils import format_number
|
||||
from app.utils.dates import (
|
||||
create_new_date,
|
||||
date_string_to_time_passed,
|
||||
seconds_to_time_string,
|
||||
)
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
|
||||
parser.add_argument("start", type=int, default=0, location="args")
|
||||
parser.add_argument("limit", type=int, default=20, location="args")
|
||||
parser.add_argument("sortby", type=str, default="created_date", location="args")
|
||||
parser.add_argument("reverse", type=str, default="1", location="args")
|
||||
|
||||
|
||||
class Albums(Resource):
|
||||
def get(self, itemtype: str):
|
||||
is_albums = itemtype == "albums"
|
||||
is_artists = itemtype == "artists"
|
||||
|
||||
items = AlbumStore.albums
|
||||
|
||||
if is_artists:
|
||||
items = ArtistStore.artists
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
start = args["start"]
|
||||
limit = args["limit"]
|
||||
sort = args["sortby"]
|
||||
reverse = args["reverse"] == "1"
|
||||
|
||||
if sort == "":
|
||||
sort = "created_date"
|
||||
|
||||
sort_is_count = sort == "count"
|
||||
sort_is_duration = sort == "duration"
|
||||
sort_is_create_date = sort == "created_date"
|
||||
|
||||
sort_is_date = is_albums and sort == "date"
|
||||
sort_is_artist = is_albums and sort == "albumartists"
|
||||
|
||||
sort_is_artist_trackcount = is_artists and sort == "trackcount"
|
||||
sort_is_artist_albumcount = is_artists and sort == "albumcount"
|
||||
|
||||
lambda_sort = lambda x: getattr(x, sort)
|
||||
if sort_is_artist:
|
||||
lambda_sort = lambda x: getattr(x, sort)[0].name
|
||||
|
||||
sorted_items = sorted(items, key=lambda_sort, reverse=reverse)
|
||||
items = sorted_items[start : start + limit]
|
||||
|
||||
album_list = []
|
||||
|
||||
for item in items:
|
||||
item_dict = serialize_album(item) if is_albums else serialize_artist(item)
|
||||
|
||||
if sort_is_date:
|
||||
item_dict["help_text"] = item.date
|
||||
|
||||
if sort_is_create_date:
|
||||
date = create_new_date(datetime.fromtimestamp(item.created_date))
|
||||
timeago = date_string_to_time_passed(date)
|
||||
item_dict["help_text"] = timeago
|
||||
|
||||
if sort_is_count:
|
||||
item_dict[
|
||||
"help_text"
|
||||
] = f"{format_number(item.count)} track{'' if item.count == 1 else 's'}"
|
||||
|
||||
if sort_is_duration:
|
||||
item_dict["help_text"] = seconds_to_time_string(item.duration)
|
||||
|
||||
if sort_is_artist_trackcount:
|
||||
item_dict[
|
||||
"help_text"
|
||||
] = f"{format_number(item.trackcount)} track{'' if item.trackcount == 1 else 's'}"
|
||||
|
||||
if sort_is_artist_albumcount:
|
||||
item_dict[
|
||||
"help_text"
|
||||
] = f"{format_number(item.albumcount)} album{'' if item.albumcount == 1 else 's'}"
|
||||
|
||||
album_list.append(item_dict)
|
||||
|
||||
return {"items": album_list, "total": len(sorted_items)}
|
||||
@@ -1,11 +1,25 @@
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
|
||||
from .recents import RecentlyAdded, RecentlyPlayed
|
||||
from app.api.apischemas import GenericLimitSchema
|
||||
from app.lib.home.recentlyadded import get_recent_items
|
||||
from app.lib.home.recentlyplayed import get_recently_played
|
||||
|
||||
api_bp = Blueprint("home", __name__, url_prefix="/home")
|
||||
api = Api(api_bp)
|
||||
bp_tag = Tag(name="Home", description="Homepage items")
|
||||
api = APIBlueprint("home", __name__, url_prefix="/home", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
api.add_resource(RecentlyAdded, "/recents/added")
|
||||
api.add_resource(RecentlyPlayed, "/recents/played")
|
||||
@api.get("/recents/added")
|
||||
def get_recently_added(query: GenericLimitSchema):
|
||||
"""
|
||||
Get recently added
|
||||
"""
|
||||
return {"items": get_recent_items(query.limit)}
|
||||
|
||||
|
||||
@api.get("/recents/played")
|
||||
def get_recent_plays(query: GenericLimitSchema):
|
||||
"""
|
||||
Get recently played
|
||||
"""
|
||||
return {"items": get_recently_played(query.limit)}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from flask_restful import Resource, reqparse
|
||||
|
||||
from app.lib.home.recentlyadded import get_recent_items
|
||||
from app.lib.home.recentlyplayed import get_recently_played
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
|
||||
parser.add_argument("limit", type=int, required=False, default=7, location="args")
|
||||
|
||||
|
||||
class RecentlyAdded(Resource):
|
||||
def get(self):
|
||||
args = parser.parse_args()
|
||||
limit = args["limit"]
|
||||
|
||||
return {"items": get_recent_items(limit)}
|
||||
|
||||
|
||||
class RecentlyPlayed(Resource):
|
||||
def get(self):
|
||||
args = parser.parse_args()
|
||||
limit = args["limit"]
|
||||
|
||||
return {"items": get_recently_played(limit)}
|
||||
+76
-42
@@ -1,88 +1,122 @@
|
||||
from pathlib import Path
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from pydantic import BaseModel, Field
|
||||
from flask import send_from_directory
|
||||
|
||||
from flask import Blueprint, send_from_directory
|
||||
from app.settings import Defaults, Paths
|
||||
|
||||
from app.settings import Paths
|
||||
|
||||
api = Blueprint("imgserver", __name__, url_prefix="/img")
|
||||
|
||||
|
||||
@api.route("/")
|
||||
def hello():
|
||||
return "<h1>Image Server</h1>"
|
||||
bp_tag = Tag(
|
||||
name="Images", description="Image filenames are constructured as '{itemhash}.webp'"
|
||||
)
|
||||
api = APIBlueprint("imgserver", __name__, url_prefix="/img", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
def send_fallback_img(filename: str = "default.webp"):
|
||||
path = Paths.get_assets_path()
|
||||
img = Path(path) / filename
|
||||
folder = Paths.get_assets_path()
|
||||
img = Path(folder) / filename
|
||||
|
||||
if not img.exists():
|
||||
return "", 404
|
||||
|
||||
return send_from_directory(path, filename)
|
||||
return send_from_directory(folder, filename)
|
||||
|
||||
|
||||
@api.route("/t/o/<imgpath>")
|
||||
def send_original_thumbnail(imgpath: str):
|
||||
path = Paths.get_original_thumb_path()
|
||||
fpath = Path(path) / imgpath
|
||||
class ImagePath(BaseModel):
|
||||
imgpath: str = Field(
|
||||
description="The image filename",
|
||||
example=Defaults.API_ALBUMHASH + ".webp",
|
||||
)
|
||||
|
||||
|
||||
@api.get("/t/o/<imgpath>")
|
||||
def send_original_thumbnail(path: ImagePath):
|
||||
"""
|
||||
Get original thumbnail
|
||||
"""
|
||||
folder = Paths.get_original_thumb_path()
|
||||
fpath = Path(folder) / path.imgpath
|
||||
|
||||
if fpath.exists():
|
||||
return send_from_directory(path, imgpath)
|
||||
return send_from_directory(folder, path.imgpath)
|
||||
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
@api.route("/t/<imgpath>")
|
||||
def send_lg_thumbnail(imgpath: str):
|
||||
path = Paths.get_lg_thumb_path()
|
||||
fpath = Path(path) / imgpath
|
||||
@api.get("/t/<imgpath>")
|
||||
def send_lg_thumbnail(path: ImagePath):
|
||||
"""
|
||||
Get large thumbnail (500 x 500)
|
||||
"""
|
||||
folder = Paths.get_lg_thumb_path()
|
||||
fpath = Path(folder) / path.imgpath
|
||||
|
||||
if fpath.exists():
|
||||
return send_from_directory(path, imgpath)
|
||||
return send_from_directory(folder, path.imgpath)
|
||||
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
@api.route("/t/s/<imgpath>")
|
||||
def send_sm_thumbnail(imgpath: str):
|
||||
path = Paths.get_sm_thumb_path()
|
||||
fpath = Path(path) / imgpath
|
||||
@api.get("/t/s/<imgpath>")
|
||||
def send_sm_thumbnail(path: ImagePath):
|
||||
"""
|
||||
Get small thumbnail (64 x 64)
|
||||
"""
|
||||
folder = Paths.get_sm_thumb_path()
|
||||
fpath = Path(folder) / path.imgpath
|
||||
|
||||
if fpath.exists():
|
||||
return send_from_directory(path, imgpath)
|
||||
return send_from_directory(folder, path.imgpath)
|
||||
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
@api.route("/a/<imgpath>")
|
||||
def send_lg_artist_image(imgpath: str):
|
||||
path = Paths.get_artist_img_lg_path()
|
||||
fpath = Path(path) / imgpath
|
||||
@api.get("/a/<imgpath>")
|
||||
def send_lg_artist_image(path: ImagePath):
|
||||
"""
|
||||
Get large artist image (500 x 500)
|
||||
"""
|
||||
folder = Paths.get_artist_img_lg_path()
|
||||
fpath = Path(folder) / path.imgpath
|
||||
|
||||
if fpath.exists():
|
||||
return send_from_directory(path, imgpath)
|
||||
return send_from_directory(folder, path.imgpath)
|
||||
|
||||
return send_fallback_img("artist.webp")
|
||||
|
||||
|
||||
@api.route("/a/s/<imgpath>")
|
||||
def send_sm_artist_image(imgpath: str):
|
||||
path = Paths.get_artist_img_sm_path()
|
||||
fpath = Path(path) / imgpath
|
||||
@api.get("/a/s/<imgpath>")
|
||||
def send_sm_artist_image(path: ImagePath):
|
||||
"""
|
||||
Get small artist image (64 x 64)
|
||||
"""
|
||||
folder = Paths.get_artist_img_sm_path()
|
||||
fpath = Path(folder) / path.imgpath
|
||||
|
||||
if fpath.exists():
|
||||
return send_from_directory(path, imgpath)
|
||||
return send_from_directory(folder, path.imgpath)
|
||||
|
||||
return send_fallback_img("artist.webp")
|
||||
|
||||
|
||||
@api.route("/p/<imgpath>")
|
||||
def send_playlist_image(imgpath: str):
|
||||
path = Paths.get_playlist_img_path()
|
||||
fpath = Path(path) / imgpath
|
||||
class PlaylistImagePath(BaseModel):
|
||||
imgpath: str = Field(
|
||||
description="The image path",
|
||||
example="1.webp",
|
||||
)
|
||||
|
||||
|
||||
@api.get("/p/<imgpath>")
|
||||
def send_playlist_image(path: PlaylistImagePath):
|
||||
"""
|
||||
Get playlist image
|
||||
|
||||
Images are constructed as '{playlist_id}.webp'
|
||||
"""
|
||||
folder = Paths.get_playlist_img_path()
|
||||
fpath = Path(folder) / path.imgpath
|
||||
|
||||
if fpath.exists():
|
||||
return send_from_directory(path, imgpath)
|
||||
return send_from_directory(folder, path.imgpath)
|
||||
|
||||
return send_fallback_img("playlist.svg")
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from pydantic import Field
|
||||
from app.api.apischemas import TrackHashSchema
|
||||
|
||||
from app.api.logger.tracks import LogTrack
|
||||
from app.db.sqlite.logger.tracks import SQLiteTrackLogger as db
|
||||
from app.settings import Defaults
|
||||
|
||||
bp_tag = Tag(name="Logger", description="Log item plays")
|
||||
api = APIBlueprint("logger", __name__, url_prefix="/logger", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
api_bp = Blueprint("logger", __name__, url_prefix="/logger")
|
||||
api = Api(api_bp)
|
||||
class LogTrackBody(TrackHashSchema):
|
||||
timestamp: int = Field(description="The timestamp of the track", example=1622217600)
|
||||
duration: int = Field(
|
||||
description="The duration of the track in seconds", example=300
|
||||
)
|
||||
source: str = Field(
|
||||
description="The play source of the track",
|
||||
example=f"al:{Defaults.API_ALBUMHASH}",
|
||||
)
|
||||
|
||||
|
||||
api.add_resource(LogTrack, "/track/log")
|
||||
@api.post("/track/log")
|
||||
def log_track(body: LogTrackBody):
|
||||
"""
|
||||
Log a track play to the database.
|
||||
"""
|
||||
trackhash = body.trackhash
|
||||
timestamp = body.timestamp
|
||||
duration = body.duration
|
||||
source = body.source
|
||||
|
||||
last_row = db.insert_track(
|
||||
trackhash=trackhash, timestamp=timestamp, duration=duration, source=source
|
||||
)
|
||||
|
||||
return {"last_row": last_row}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
from flask_restful import Resource, reqparse
|
||||
from app.db.sqlite.logger.tracks import SQLiteTrackLogger as db
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("trackhash", type=str, required=True)
|
||||
parser.add_argument("timestamp", type=int, required=True)
|
||||
parser.add_argument("duration", type=int, required=True)
|
||||
parser.add_argument("source", type=str, required=True)
|
||||
|
||||
|
||||
class LogTrack(Resource):
|
||||
def post(self):
|
||||
args = parser.parse_args(strict=True)
|
||||
|
||||
last_row = db.insert_track(
|
||||
args["trackhash"], args["duration"], args["source"], args["timestamp"]
|
||||
)
|
||||
|
||||
return {"last_row": last_row}
|
||||
+24
-21
@@ -1,5 +1,8 @@
|
||||
from flask import Blueprint, request
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from pydantic import Field
|
||||
|
||||
from app.api.apischemas import TrackHashSchema
|
||||
from app.lib.lyrics import (
|
||||
get_lyrics,
|
||||
check_lyrics_file,
|
||||
@@ -7,21 +10,24 @@ from app.lib.lyrics import (
|
||||
get_lyrics_from_tags,
|
||||
)
|
||||
|
||||
api = Blueprint("lyrics", __name__, url_prefix="")
|
||||
bp_tag = Tag(name="Lyrics", description="Get lyrics")
|
||||
api = APIBlueprint("lyrics", __name__, url_prefix="/lyrics", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
@api.route("/lyrics", methods=["POST"])
|
||||
def send_lyrics():
|
||||
class SendLyricsBody(TrackHashSchema):
|
||||
filepath: str = Field(
|
||||
description="The path to the file",
|
||||
example="/path/to/file.mp3",
|
||||
)
|
||||
|
||||
|
||||
@api.post("")
|
||||
def send_lyrics(body: SendLyricsBody):
|
||||
"""
|
||||
Returns the lyrics for a track
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
filepath = data.get("filepath", None)
|
||||
trackhash = data.get("trackhash", None)
|
||||
|
||||
if filepath is None or trackhash is None:
|
||||
return {"error": "No filepath or trackhash provided"}, 400
|
||||
filepath = body.filepath
|
||||
trackhash = body.trackhash
|
||||
|
||||
is_synced = True
|
||||
lyrics, copyright = get_lyrics(filepath)
|
||||
@@ -38,15 +44,13 @@ def send_lyrics():
|
||||
return {"lyrics": lyrics, "synced": is_synced, "copyright": copyright}, 200
|
||||
|
||||
|
||||
@api.route("/lyrics/check", methods=["POST"])
|
||||
def check_lyrics():
|
||||
data = request.get_json()
|
||||
|
||||
filepath = data.get("filepath", None)
|
||||
trackhash = data.get("trackhash", None)
|
||||
|
||||
if filepath is None or trackhash is None:
|
||||
return {"error": "No filepath or trackhash provided"}, 400
|
||||
@api.post("/check")
|
||||
def check_lyrics(body: SendLyricsBody):
|
||||
"""
|
||||
Checks if lyrics exist for a track
|
||||
"""
|
||||
filepath = body.filepath
|
||||
trackhash = body.trackhash
|
||||
|
||||
exists = check_lyrics_file(filepath, trackhash)
|
||||
|
||||
@@ -56,4 +60,3 @@ def check_lyrics():
|
||||
exists = get_lyrics_from_tags(filepath, just_check=True)
|
||||
|
||||
return {"exists": exists}, 200
|
||||
|
||||
|
||||
+117
-110
@@ -1,12 +1,15 @@
|
||||
"""
|
||||
All playlist-related routes.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
import pathlib
|
||||
|
||||
from flask import Blueprint, request
|
||||
from PIL import UnidentifiedImageError, Image
|
||||
from pydantic import BaseModel, Field
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint, FileStorage
|
||||
|
||||
from app import models
|
||||
from app.db.sqlite.playlists import SQLitePlaylistMethods
|
||||
@@ -18,23 +21,25 @@ from app.utils.dates import create_new_date, date_string_to_time_passed
|
||||
from app.utils.remove_duplicates import remove_duplicates
|
||||
from app.settings import Paths
|
||||
|
||||
api = Blueprint("playlist", __name__, url_prefix="/")
|
||||
tag = Tag(name="Playlists", description="Get and manage playlists")
|
||||
api = APIBlueprint("playlists", __name__, url_prefix="/playlists", abp_tags=[tag])
|
||||
|
||||
PL = SQLitePlaylistMethods
|
||||
|
||||
class SendAllPlaylistsQuery(BaseModel):
|
||||
no_images: bool = Field(False, description="Whether to include images")
|
||||
|
||||
@api.route("/playlists", methods=["GET"])
|
||||
def send_all_playlists():
|
||||
|
||||
@api.get("")
|
||||
def send_all_playlists(query: SendAllPlaylistsQuery):
|
||||
"""
|
||||
Gets all the playlists.
|
||||
"""
|
||||
no_images = request.args.get("no_images", False)
|
||||
|
||||
playlists = PL.get_all_playlists()
|
||||
playlists = list(playlists)
|
||||
|
||||
for playlist in playlists:
|
||||
if not no_images:
|
||||
if not query.no_images:
|
||||
playlist.images = playlistlib.get_first_4_images(
|
||||
trackhashes=playlist.trackhashes
|
||||
)
|
||||
@@ -69,22 +74,23 @@ def insert_playlist(name: str, image: str = None):
|
||||
return PL.insert_one_playlist(playlist)
|
||||
|
||||
|
||||
@api.route("/playlist/new", methods=["POST"])
|
||||
def create_playlist():
|
||||
class CreatePlaylistBody(BaseModel):
|
||||
name: str = Field(..., description="The name of the playlist")
|
||||
|
||||
|
||||
@api.post("/new")
|
||||
def create_playlist(body: CreatePlaylistBody):
|
||||
"""
|
||||
New playlist
|
||||
|
||||
Creates a new playlist. Accepts POST method with a JSON body.
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return {"error": "Playlist name not provided"}, 400
|
||||
|
||||
existing_playlist_count = PL.count_playlist_by_name(data["name"])
|
||||
existing_playlist_count = PL.count_playlist_by_name(body.name)
|
||||
|
||||
if existing_playlist_count > 0:
|
||||
return {"error": "Playlist already exists"}, 409
|
||||
|
||||
playlist = insert_playlist(data["name"])
|
||||
playlist = insert_playlist(body.name)
|
||||
|
||||
if playlist is None:
|
||||
return {"error": "Playlist could not be created"}, 500
|
||||
@@ -119,25 +125,30 @@ def get_artist_trackhashes(artisthash: str):
|
||||
return [t.trackhash for t in tracks]
|
||||
|
||||
|
||||
@api.route("/playlist/<playlist_id>/add", methods=["POST"])
|
||||
def add_item_to_playlist(playlist_id: str):
|
||||
class PlaylistIDPath(BaseModel):
|
||||
# INFO: playlistid string examples: "recentlyadded"
|
||||
playlistid: int | str = Field(..., description="The ID of the playlist")
|
||||
|
||||
|
||||
class AddItemToPlaylistBody(BaseModel):
|
||||
itemtype: str = Field(
|
||||
default="tracks",
|
||||
description="The type of item to add",
|
||||
examples=["tracks", "folder", "album", "artist"],
|
||||
)
|
||||
itemhash: str = Field(..., description="The hash of the item to add")
|
||||
|
||||
|
||||
@api.post("/<playlistid>/add")
|
||||
def add_item_to_playlist(path: PlaylistIDPath, body: AddItemToPlaylistBody):
|
||||
"""
|
||||
Takes a playlist ID and a track hash, and adds the track to the playlist
|
||||
Add to playlist.
|
||||
|
||||
If itemtype is not "tracks", itemhash is expected to be a folder, album or artist hash.
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return {"error": "Track hash not provided"}, 400
|
||||
|
||||
try:
|
||||
itemtype = data["itemtype"]
|
||||
except KeyError:
|
||||
itemtype = None
|
||||
|
||||
try:
|
||||
itemhash: str = data["itemhash"]
|
||||
except KeyError:
|
||||
itemhash = None
|
||||
itemtype = body.itemtype
|
||||
itemhash = body.itemhash
|
||||
playlist_id = path.playlistid
|
||||
|
||||
if itemtype == "tracks":
|
||||
trackhashes = itemhash.split(",")
|
||||
@@ -158,13 +169,17 @@ def add_item_to_playlist(playlist_id: str):
|
||||
return {"msg": "Done"}, 200
|
||||
|
||||
|
||||
@api.route("/playlist/<playlistid>")
|
||||
def get_playlist(playlistid: str):
|
||||
class GetPlaylistQuery(BaseModel):
|
||||
no_tracks: bool = Field(False, description="Whether to include tracks")
|
||||
|
||||
|
||||
@api.get("/<playlistid>")
|
||||
def get_playlist(path: PlaylistIDPath, query: GetPlaylistQuery):
|
||||
"""
|
||||
Gets a playlist by id, and if it exists, it gets all the tracks in the playlist and returns them.
|
||||
Get playlist by id
|
||||
"""
|
||||
no_tracks = request.args.get("no_tracks", "false")
|
||||
no_tracks = no_tracks == "true"
|
||||
no_tracks = query.no_tracks
|
||||
playlistid = path.playlistid
|
||||
|
||||
is_recently_added = playlistid == "recentlyadded"
|
||||
|
||||
@@ -201,31 +216,40 @@ def get_playlist(playlistid: str):
|
||||
return {"info": playlist, "tracks": tracks if not no_tracks else []}
|
||||
|
||||
|
||||
@api.route("/playlist/<playlistid>/update", methods=["PUT"])
|
||||
def update_playlist_info(playlistid: str):
|
||||
if playlistid is None:
|
||||
return {"error": "Playlist ID not provided"}, 400
|
||||
class UpdatePlaylistForm(BaseModel):
|
||||
image: FileStorage = Field(None, description="The image file")
|
||||
name: str = Field(..., description="The name of the playlist")
|
||||
settings: str = Field(
|
||||
...,
|
||||
description="The settings of the playlist",
|
||||
example='{"has_gif": false, "banner_pos": 50, "square_img": false, "pinned": false}',
|
||||
)
|
||||
|
||||
db_playlist = PL.get_playlist_by_id(int(playlistid))
|
||||
|
||||
@api.put("/<playlistid>/update", methods=["PUT"])
|
||||
def update_playlist_info(path: PlaylistIDPath, form: UpdatePlaylistForm):
|
||||
"""
|
||||
Update playlist
|
||||
"""
|
||||
playlistid = path.playlistid
|
||||
db_playlist = PL.get_playlist_by_id(playlistid)
|
||||
|
||||
if db_playlist is None:
|
||||
return {"error": "Playlist not found"}, 404
|
||||
|
||||
image = None
|
||||
image = form.image
|
||||
|
||||
if "image" in request.files:
|
||||
image = request.files["image"]
|
||||
if form.image:
|
||||
image = form.image
|
||||
|
||||
data = request.form
|
||||
|
||||
settings = json.loads(data.get("settings"))
|
||||
settings = json.loads(form.settings)
|
||||
settings["has_gif"] = False
|
||||
|
||||
playlist = {
|
||||
"id": int(playlistid),
|
||||
"id": playlistid,
|
||||
"image": db_playlist.image,
|
||||
"last_updated": create_new_date(),
|
||||
"name": str(data.get("name")).strip(),
|
||||
"name": str(form.name).strip(),
|
||||
"settings": settings,
|
||||
"trackhashes": json.dumps([]),
|
||||
}
|
||||
@@ -247,7 +271,7 @@ def update_playlist_info(playlistid: str):
|
||||
|
||||
p_tuple = (*playlist.values(),)
|
||||
|
||||
PL.update_playlist(int(playlistid), playlist)
|
||||
PL.update_playlist(playlistid, playlist)
|
||||
|
||||
playlist = models.Playlist(*p_tuple)
|
||||
playlist.last_updated = date_string_to_time_passed(playlist.last_updated)
|
||||
@@ -257,12 +281,12 @@ def update_playlist_info(playlistid: str):
|
||||
}
|
||||
|
||||
|
||||
@api.route("/playlist/<playlistid>/pin_unpin", methods=["GET"])
|
||||
def pin_unpin_playlist(playlistid: str):
|
||||
@api.post("/<playlistid>/pin_unpin")
|
||||
def pin_unpin_playlist(path: PlaylistIDPath):
|
||||
"""
|
||||
Pins or unpins a playlist.
|
||||
Pin playlist.
|
||||
"""
|
||||
playlist = PL.get_playlist_by_id(int(playlistid))
|
||||
playlist = PL.get_playlist_by_id(path.playlistid)
|
||||
|
||||
if playlist is None:
|
||||
return {"error": "Playlist not found"}, 404
|
||||
@@ -274,23 +298,22 @@ def pin_unpin_playlist(playlistid: str):
|
||||
except KeyError:
|
||||
settings["pinned"] = True
|
||||
|
||||
PL.update_settings(int(playlistid), settings)
|
||||
PL.update_settings(path.playlistid, settings)
|
||||
|
||||
return {"msg": "Done"}, 200
|
||||
|
||||
|
||||
@api.route("/playlist/<playlistid>/remove-img", methods=["GET"])
|
||||
def remove_playlist_image(playlistid: str):
|
||||
@api.delete("/<playlistid>/remove-img")
|
||||
def remove_playlist_image(path: PlaylistIDPath):
|
||||
"""
|
||||
Removes the playlist image.
|
||||
Clear playlist image.
|
||||
"""
|
||||
pid = int(playlistid)
|
||||
playlist = PL.get_playlist_by_id(pid)
|
||||
playlist = PL.get_playlist_by_id(path.playlistid)
|
||||
|
||||
if playlist is None:
|
||||
return {"error": "Playlist not found"}, 404
|
||||
|
||||
PL.remove_banner(pid)
|
||||
PL.remove_banner(path.playlistid)
|
||||
|
||||
playlist.image = None
|
||||
playlist.thumb = None
|
||||
@@ -303,78 +326,62 @@ def remove_playlist_image(playlistid: str):
|
||||
return {"playlist": playlist}, 200
|
||||
|
||||
|
||||
@api.route("/playlist/delete", methods=["POST"])
|
||||
def remove_playlist():
|
||||
@api.delete("/<playlistid>/delete", methods=["DELETE"])
|
||||
def remove_playlist(path: PlaylistIDPath):
|
||||
"""
|
||||
Deletes a playlist by ID.
|
||||
Delete playlist
|
||||
"""
|
||||
message = {"error": "Playlist ID not provided"}
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return message, 400
|
||||
|
||||
try:
|
||||
pid = data["pid"]
|
||||
except KeyError:
|
||||
return message, 400
|
||||
|
||||
PL.delete_playlist(pid)
|
||||
PL.delete_playlist(path.playlistid)
|
||||
|
||||
return {"msg": "Done"}, 200
|
||||
|
||||
|
||||
@api.route("/playlist/<pid>/remove-tracks", methods=["POST"])
|
||||
def remove_tracks_from_playlist(pid: int):
|
||||
data = request.get_json()
|
||||
class RemoveTracksFromPlaylistBody(BaseModel):
|
||||
tracks: list[dict] = Field(..., description="A list of trackhashes to remove")
|
||||
|
||||
if data is None:
|
||||
return {"error": "Track index not provided"}, 400
|
||||
|
||||
@api.post("/<playlistid>/remove-tracks")
|
||||
def remove_tracks_from_playlist(
|
||||
path: PlaylistIDPath, body: RemoveTracksFromPlaylistBody
|
||||
):
|
||||
"""
|
||||
Remove track from playlist
|
||||
"""
|
||||
# A track looks like this:
|
||||
# {
|
||||
# trackhash: str;
|
||||
# index: int;
|
||||
# }
|
||||
|
||||
tracks = data["tracks"]
|
||||
PL.remove_tracks_from_playlist(pid, tracks)
|
||||
PL.remove_tracks_from_playlist(path.playlistid, body.tracks)
|
||||
|
||||
return {"msg": "Done"}, 200
|
||||
|
||||
|
||||
def playlist_exists(name: str) -> bool:
|
||||
def playlist_name_exists(name: str) -> bool:
|
||||
return PL.count_playlist_by_name(name) > 0
|
||||
|
||||
|
||||
@api.route("/playlist/save-item", methods=["POST"])
|
||||
def save_item_as_playlist():
|
||||
data = request.get_json()
|
||||
msg = {"error": "'itemtype', 'playlist_name' and 'itemhash' not provided"}, 400
|
||||
class SavePlaylistAsItemBody(BaseModel):
|
||||
itemtype: str = Field(..., description="The type of item", example="tracks")
|
||||
playlist_name: str = Field(..., description="The name of the playlist")
|
||||
itemhash: str = Field(..., description="The hash of the item to save")
|
||||
|
||||
if data is None:
|
||||
return msg
|
||||
|
||||
try:
|
||||
playlist_name = data["playlist_name"]
|
||||
except KeyError:
|
||||
playlist_name = None
|
||||
@api.post("/save-item")
|
||||
def save_item_as_playlist(body: SavePlaylistAsItemBody):
|
||||
"""
|
||||
Save as playlist
|
||||
|
||||
if playlist_exists(playlist_name):
|
||||
Saves a track, album, artist or folder as a playlist
|
||||
"""
|
||||
itemtype = body.itemtype
|
||||
playlist_name = body.playlist_name
|
||||
itemhash = body.itemhash
|
||||
|
||||
if playlist_name_exists(playlist_name):
|
||||
return {"error": "Playlist already exists"}, 409
|
||||
|
||||
try:
|
||||
itemtype = data["itemtype"]
|
||||
except KeyError:
|
||||
itemtype = None
|
||||
|
||||
try:
|
||||
itemhash: str = data["itemhash"]
|
||||
except KeyError:
|
||||
itemhash = None
|
||||
|
||||
if itemtype is None or playlist_name is None or itemhash is None:
|
||||
return msg
|
||||
|
||||
if itemtype == "tracks":
|
||||
trackhashes = itemhash.split(",")
|
||||
elif itemtype == "folder":
|
||||
|
||||
+38
-15
@@ -1,37 +1,60 @@
|
||||
from flask import Blueprint, request
|
||||
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from pydantic import BaseModel, Field
|
||||
from app.db.sqlite.plugins import PluginsMethods
|
||||
|
||||
|
||||
api = Blueprint("plugins", __name__, url_prefix="/plugins")
|
||||
bp_tag = Tag(name="Plugins", description="Manage plugins")
|
||||
api = APIBlueprint("plugins", __name__, url_prefix="/plugins", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
@api.route("/", methods=["GET"])
|
||||
@api.get("/")
|
||||
def get_all_plugins():
|
||||
"""
|
||||
List all plugins
|
||||
"""
|
||||
plugins = PluginsMethods.get_all_plugins()
|
||||
|
||||
return {"plugins": plugins}
|
||||
|
||||
|
||||
@api.route("/setactive", methods=["GET"])
|
||||
def activate_deactivate_plugin():
|
||||
name = request.args.get("plugin", None)
|
||||
state = request.args.get("state", None)
|
||||
class PluginBody(BaseModel):
|
||||
plugin: str = Field(description="The plugin name", example="lyrics")
|
||||
|
||||
if not name or not state:
|
||||
return {"error": "Missing plugin or state"}, 400
|
||||
|
||||
PluginsMethods.plugin_set_active(name, int(state))
|
||||
class PluginActivateBody(PluginBody):
|
||||
active: bool = Field(
|
||||
description="New plugin active state", example=False, default=False
|
||||
)
|
||||
|
||||
|
||||
@api.post("/setactive")
|
||||
def activate_deactivate_plugin(body: PluginActivateBody):
|
||||
"""
|
||||
Activate/Deactivate plugin
|
||||
"""
|
||||
name = body.plugin
|
||||
active = 1 if body.active else 0
|
||||
|
||||
PluginsMethods.plugin_set_active(name, active)
|
||||
|
||||
return {"message": "OK"}, 200
|
||||
|
||||
|
||||
@api.route("/settings", methods=["POST"])
|
||||
def update_plugin_settings():
|
||||
data = request.get_json()
|
||||
class PluginSettingsBody(PluginBody):
|
||||
settings: dict = Field(
|
||||
description="The new plugin settings", example={"key": "value"}
|
||||
)
|
||||
|
||||
plugin = data.get("plugin", None)
|
||||
settings = data.get("settings", None)
|
||||
|
||||
@api.post("/settings")
|
||||
def update_plugin_settings(body: PluginSettingsBody):
|
||||
"""
|
||||
Update plugin settings
|
||||
"""
|
||||
plugin = body.plugin
|
||||
settings = body.settings
|
||||
|
||||
if not plugin or not settings:
|
||||
return {"error": "Missing plugin or settings"}, 400
|
||||
|
||||
+28
-11
@@ -1,24 +1,41 @@
|
||||
from flask import Blueprint, request
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from pydantic import Field
|
||||
from app.api.apischemas import TrackHashSchema
|
||||
from app.lib.lyrics import format_synced_lyrics
|
||||
|
||||
from app.plugins.lyrics import Lyrics
|
||||
from app.settings import Defaults
|
||||
from app.utils.hashing import create_hash
|
||||
|
||||
api = Blueprint("lyricsplugin", __name__, url_prefix="/plugins/lyrics")
|
||||
bp_tag = Tag(name="Lyrics Plugin", description="Musixmatch lyrics plugin")
|
||||
api = APIBlueprint(
|
||||
"lyricsplugin", __name__, url_prefix="/plugins/lyrics", abp_tags=[bp_tag]
|
||||
)
|
||||
|
||||
|
||||
@api.route("/search", methods=["POST"])
|
||||
def search_lyrics():
|
||||
data = request.get_json()
|
||||
class LyricsSearchBody(TrackHashSchema):
|
||||
title: str = Field(description="The track title ", example=Defaults.API_TRACKNAME)
|
||||
artist: str = Field(description="The track artist ", example=Defaults.API_ARTISTNAME)
|
||||
album: str = Field(description="The track track album ", example=Defaults.API_ALBUMNAME)
|
||||
filepath: str = Field(
|
||||
description="Track filepath to save the lyrics file relative to",
|
||||
example="/home/cwilvx/temp/crazy song.mp3",
|
||||
)
|
||||
|
||||
trackhash = data.get("trackhash", "")
|
||||
title = data.get("title", "")
|
||||
artist = data.get("artist", "")
|
||||
album = data.get("album", "")
|
||||
filepath = data.get("filepath", None)
|
||||
|
||||
@api.post("/search")
|
||||
def search_lyrics(body: LyricsSearchBody):
|
||||
"""
|
||||
Search for lyrics by title and artist
|
||||
"""
|
||||
title = body.title
|
||||
artist = body.artist
|
||||
album = body.album
|
||||
filepath = body.filepath
|
||||
trackhash = body.trackhash
|
||||
|
||||
finder = Lyrics()
|
||||
|
||||
data = finder.search_lyrics_by_title_and_artist(title, artist)
|
||||
|
||||
if not data:
|
||||
|
||||
+54
-37
@@ -2,14 +2,19 @@
|
||||
Contains all the search routes.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask import request
|
||||
from unidecode import unidecode
|
||||
from pydantic import BaseModel, Field
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
|
||||
from app import models
|
||||
from app.lib import searchlib
|
||||
from app.settings import Defaults
|
||||
from app.store.tracks import TrackStore
|
||||
|
||||
api = Blueprint("search", __name__, url_prefix="/")
|
||||
tag = Tag(name="Search", description="Search for tracks, albums and artists")
|
||||
api = APIBlueprint("search", __name__, url_prefix="/search", abp_tags=[tag])
|
||||
|
||||
SEARCH_COUNT = 30
|
||||
"""The max amount of items to return per request"""
|
||||
@@ -63,18 +68,19 @@ class Search:
|
||||
return finder.search(self.query, in_quotes=in_quotes, limit=limit)
|
||||
|
||||
|
||||
@api.route("/search/tracks", methods=["GET"])
|
||||
def search_tracks():
|
||||
class SearchQuery(BaseModel):
|
||||
q: str = Field(description="The search query", example=Defaults.API_ARTISTNAME)
|
||||
|
||||
|
||||
@api.get("/tracks")
|
||||
def search_tracks(query: SearchQuery):
|
||||
"""
|
||||
Searches for tracks that match the search query.
|
||||
Search tracks
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
query = query.q
|
||||
in_quotes = query_in_quotes(query)
|
||||
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
tracks = Search(query).search_tracks(in_quotes)
|
||||
|
||||
return {
|
||||
@@ -83,18 +89,15 @@ def search_tracks():
|
||||
}
|
||||
|
||||
|
||||
@api.route("/search/albums", methods=["GET"])
|
||||
def search_albums():
|
||||
@api.get("/albums")
|
||||
def search_albums(query: SearchQuery):
|
||||
"""
|
||||
Searches for albums.
|
||||
Search albums.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
query = query.q
|
||||
in_quotes = query_in_quotes(query)
|
||||
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
albums = Search(query).search_albums(in_quotes)
|
||||
|
||||
return {
|
||||
@@ -103,13 +106,13 @@ def search_albums():
|
||||
}
|
||||
|
||||
|
||||
@api.route("/search/artists", methods=["GET"])
|
||||
def search_artists():
|
||||
@api.get("/artists")
|
||||
def search_artists(query: SearchQuery):
|
||||
"""
|
||||
Searches for artists.
|
||||
Search artists.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
query = query.q
|
||||
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
@@ -122,15 +125,21 @@ def search_artists():
|
||||
}
|
||||
|
||||
|
||||
@api.route("/search/top", methods=["GET"])
|
||||
def get_top_results():
|
||||
"""
|
||||
Returns the top results for the search query.
|
||||
"""
|
||||
class TopResultsQuery(SearchQuery):
|
||||
limit: int = Field(
|
||||
description="The number of items to return", default=Defaults.API_CARD_LIMIT
|
||||
)
|
||||
|
||||
query = request.args.get("q")
|
||||
limit = request.args.get("limit", "6")
|
||||
limit = int(limit)
|
||||
|
||||
@api.get("/top")
|
||||
def get_top_results(query: TopResultsQuery):
|
||||
"""
|
||||
Get top results
|
||||
|
||||
Returns the top results for the given query.
|
||||
"""
|
||||
limit = query.limit
|
||||
query = query.q
|
||||
|
||||
in_quotes = query_in_quotes(query)
|
||||
|
||||
@@ -140,32 +149,40 @@ def get_top_results():
|
||||
return Search(query).get_top_results(in_quotes=in_quotes, limit=limit)
|
||||
|
||||
|
||||
@api.route("/search/loadmore")
|
||||
def search_load_more():
|
||||
class SearchLoadMoreQuery(SearchQuery):
|
||||
type: str = Field(description="The type of search", example="tracks")
|
||||
index: int = Field(description="The index to start from", default=0)
|
||||
|
||||
|
||||
@api.get("/loadmore")
|
||||
def search_load_more(query: SearchLoadMoreQuery):
|
||||
"""
|
||||
Load more
|
||||
|
||||
Returns more songs, albums or artists from a search query.
|
||||
|
||||
NOTE: You must first initiate a search using the `/search` endpoint.
|
||||
"""
|
||||
query = request.args.get("q")
|
||||
query = query.q
|
||||
item_type = query.type
|
||||
index = query.index
|
||||
in_quotes = query_in_quotes(query)
|
||||
|
||||
s_type = request.args.get("type")
|
||||
index = int(request.args.get("index") or 0)
|
||||
|
||||
if s_type == "tracks":
|
||||
if item_type == "tracks":
|
||||
t = Search(query).search_tracks(in_quotes)
|
||||
return {
|
||||
"tracks": t[index : index + SEARCH_COUNT],
|
||||
"more": len(t) > index + SEARCH_COUNT,
|
||||
}
|
||||
|
||||
elif s_type == "albums":
|
||||
elif item_type == "albums":
|
||||
a = Search(query).search_albums(in_quotes)
|
||||
return {
|
||||
"albums": a[index : index + SEARCH_COUNT],
|
||||
"more": len(a) > index + SEARCH_COUNT,
|
||||
}
|
||||
|
||||
elif s_type == "artists":
|
||||
elif item_type == "artists":
|
||||
a = Search(query).search_artists()
|
||||
return {
|
||||
"artists": a[index : index + SEARCH_COUNT],
|
||||
|
||||
+43
-15
@@ -1,30 +1,43 @@
|
||||
"""
|
||||
Contains all the track routes.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from flask import Blueprint, send_file, request
|
||||
from flask_openapi3 import APIBlueprint, Tag
|
||||
from pydantic import BaseModel, Field
|
||||
from app.api.apischemas import TrackHashSchema
|
||||
from app.lib.trackslib import get_silence_paddings
|
||||
|
||||
from app.store.tracks import TrackStore
|
||||
|
||||
api = Blueprint("track", __name__, url_prefix="/")
|
||||
bp_tag = Tag(name="File", description="Audio files")
|
||||
api = APIBlueprint("track", __name__, url_prefix="/file", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
@api.route("/file/<trackhash>")
|
||||
def send_track_file(trackhash: str):
|
||||
class SendTrackFileQuery(BaseModel):
|
||||
filepath: str = Field(
|
||||
description="The filepath to play (if available)", default=None
|
||||
)
|
||||
|
||||
|
||||
@api.get("/<trackhash>")
|
||||
def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery):
|
||||
"""
|
||||
Returns an audio file that matches the passed id to the client.
|
||||
Falls back to track hash if id is not found.
|
||||
Get file
|
||||
|
||||
Returns a playable audio file that corresponds to the given filepath. Falls back to track hash if filepath is not found.
|
||||
"""
|
||||
trackhash = path.trackhash
|
||||
filepath = query.filepath
|
||||
msg = {"msg": "File Not Found"}
|
||||
|
||||
def get_mime(filename: str) -> str:
|
||||
ext = filename.rsplit(".", maxsplit=1)[-1]
|
||||
return f"audio/{ext}"
|
||||
|
||||
filepath = request.args.get("filepath")
|
||||
|
||||
# If filepath is provide, try to send that
|
||||
if filepath is not None:
|
||||
try:
|
||||
track = TrackStore.get_tracks_by_filepaths([filepath])[0]
|
||||
@@ -37,9 +50,7 @@ def send_track_file(trackhash: str):
|
||||
audio_type = get_mime(filepath)
|
||||
return send_file(filepath, mimetype=audio_type)
|
||||
|
||||
if trackhash is None:
|
||||
return msg, 404
|
||||
|
||||
# Else, find file by trackhash
|
||||
tracks = TrackStore.get_tracks_by_trackhashes([trackhash])
|
||||
|
||||
for track in tracks:
|
||||
@@ -56,11 +67,28 @@ def send_track_file(trackhash: str):
|
||||
return msg, 404
|
||||
|
||||
|
||||
@api.route("/file/silence", methods=["POST"])
|
||||
def get_audio_silence():
|
||||
data = request.get_json()
|
||||
ending_file = data.get("end", None) # ending file's filepath
|
||||
starting_file = data.get("start", None) # starting file's filepath
|
||||
class GetAudioSilenceBody(BaseModel):
|
||||
ending_file: str = Field(
|
||||
description="The ending file's path",
|
||||
example="/home/cwilvx/Music/Made in Kenya/Sol generation/Bensoul - Salama.mp3",
|
||||
)
|
||||
starting_file: str = Field(
|
||||
description="The beginning file's path",
|
||||
example="/home/cwilvx/Music/Tidal/Albums/Bensoul - Qwarantunes/Bensoul - Peddi.m4a",
|
||||
)
|
||||
|
||||
|
||||
@api.post("/silence")
|
||||
def get_audio_silence(body: GetAudioSilenceBody):
|
||||
"""
|
||||
Get silence paddings
|
||||
|
||||
Returns the duration of silence at the end of the current ending track and the duration of silence at the beginning of the next track.
|
||||
|
||||
NOTE: Durations are in milliseconds.
|
||||
"""
|
||||
ending_file = body.ending_file # ending file's filepath
|
||||
starting_file = body.starting_file # starting file's filepath
|
||||
|
||||
if ending_file is None or starting_file is None:
|
||||
return {"msg": "No filepath provided"}, 400
|
||||
|
||||
+46
-26
@@ -1,4 +1,8 @@
|
||||
from flask import Blueprint, request
|
||||
from typing import Any
|
||||
from flask import request
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.db.sqlite.plugins import PluginsMethods as pdb
|
||||
from app.db.sqlite.settings import SettingsSQLMethods as sdb
|
||||
@@ -12,7 +16,8 @@ from app.store.tracks import TrackStore
|
||||
from app.utils.generators import get_random_str
|
||||
from app.utils.threading import background
|
||||
|
||||
api = Blueprint("settings", __name__, url_prefix="")
|
||||
bp_tag = Tag(name="Settings", description="Customize stuff")
|
||||
api = APIBlueprint("settings", __name__, url_prefix="/notsettings", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
def get_child_dirs(parent: str, children: list[str]):
|
||||
@@ -77,23 +82,24 @@ def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]):
|
||||
rebuild_store(db_dirs_)
|
||||
|
||||
|
||||
@api.route("/settings/add-root-dirs", methods=["POST"])
|
||||
def add_root_dirs():
|
||||
class AddRootDirsBody(BaseModel):
|
||||
new_dirs: list[str] = Field(
|
||||
description="The new directories to add",
|
||||
example=["/home/user/Music", "/home/user/Downloads"],
|
||||
)
|
||||
removed: list[str] = Field(
|
||||
description="The directories to remove",
|
||||
example=["/home/user/Downloads"],
|
||||
)
|
||||
|
||||
|
||||
@api.post("/add-root-dirs")
|
||||
def add_root_dirs(body: AddRootDirsBody):
|
||||
"""
|
||||
Add custom root directories to the database.
|
||||
"""
|
||||
msg = {"msg": "Failed! No directories were given."}
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return msg, 400
|
||||
|
||||
try:
|
||||
new_dirs: list[str] = data["new_dirs"]
|
||||
removed_dirs: list[str] = data["removed"]
|
||||
except KeyError:
|
||||
return msg, 400
|
||||
new_dirs = body.new_dirs
|
||||
removed_dirs = body.removed
|
||||
|
||||
db_dirs = sdb.get_root_dirs()
|
||||
_h = "$home"
|
||||
@@ -132,10 +138,10 @@ def add_root_dirs():
|
||||
return {"root_dirs": db_dirs}
|
||||
|
||||
|
||||
@api.route("/settings/get-root-dirs", methods=["GET"])
|
||||
@api.get("/get-root-dirs")
|
||||
def get_root_dirs():
|
||||
"""
|
||||
Get custom root directories from the database.
|
||||
Get root directories
|
||||
"""
|
||||
dirs = sdb.get_root_dirs()
|
||||
|
||||
@@ -154,10 +160,10 @@ mapp = {
|
||||
}
|
||||
|
||||
|
||||
@api.route("/settings/", methods=["GET"])
|
||||
@api.get("")
|
||||
def get_all_settings():
|
||||
"""
|
||||
Get all settings from the database.
|
||||
Get all settings
|
||||
"""
|
||||
|
||||
settings = sdb.get_all_settings()
|
||||
@@ -195,10 +201,24 @@ def reload_all_for_set_setting():
|
||||
reload_everything(get_random_str())
|
||||
|
||||
|
||||
@api.route("/settings/set", methods=["POST"])
|
||||
def set_setting():
|
||||
key = request.get_json().get("key")
|
||||
value = request.get_json().get("value")
|
||||
class SetSettingBody(BaseModel):
|
||||
key: str = Field(
|
||||
description="The setting key",
|
||||
example="artist_separators",
|
||||
)
|
||||
value: Any = Field(
|
||||
description="The setting value",
|
||||
example=",",
|
||||
)
|
||||
|
||||
|
||||
@api.post("/set")
|
||||
def set_setting(body: SetSettingBody):
|
||||
"""
|
||||
Set a setting.
|
||||
"""
|
||||
key = body.key
|
||||
value = body.value
|
||||
|
||||
if key is None or value is None or key == "root_dirs":
|
||||
return {"msg": "Invalid arguments!"}, 400
|
||||
@@ -235,10 +255,10 @@ def run_populate():
|
||||
populate.Populate(instance_key=get_random_str())
|
||||
|
||||
|
||||
@api.route("/settings/trigger-scan", methods=["GET"])
|
||||
@api.get("/trigger-scan")
|
||||
def trigger_scan():
|
||||
"""
|
||||
Triggers a scan.
|
||||
Triggers scan for new music
|
||||
"""
|
||||
run_populate()
|
||||
|
||||
|
||||
+11
-1
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Handles arguments passed to the program.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
@@ -9,6 +10,7 @@ import PyInstaller.__main__ as bundler
|
||||
from app import settings
|
||||
from app.logger import log
|
||||
from app.print_help import HELP_MESSAGE
|
||||
from app.utils.paths import getFlaskOpenApiPath
|
||||
from app.utils.xdg_utils import get_xdg_config_dir
|
||||
from app.utils.wintools import is_windows
|
||||
|
||||
@@ -45,6 +47,8 @@ class HandleArgs:
|
||||
|
||||
config_keys = [
|
||||
"SWINGMUSIC_APP_VERSION",
|
||||
"GIT_LATEST_COMMIT_HASH",
|
||||
"GIT_CURRENT_BRANCH",
|
||||
]
|
||||
|
||||
lines = []
|
||||
@@ -65,6 +69,8 @@ class HandleArgs:
|
||||
|
||||
_s = ";" if is_windows() else ":"
|
||||
|
||||
flask_openapi_path = getFlaskOpenApiPath()
|
||||
|
||||
bundler.run(
|
||||
[
|
||||
"manage.py",
|
||||
@@ -74,6 +80,7 @@ class HandleArgs:
|
||||
"--clean",
|
||||
f"--add-data=assets{_s}assets",
|
||||
f"--add-data=client{_s}client",
|
||||
f"--add-data={flask_openapi_path}/templates/static{_s}flask_openapi3/templates/static",
|
||||
f"--icon=assets/logo-fill.light.ico",
|
||||
"-y",
|
||||
]
|
||||
@@ -176,5 +183,8 @@ class HandleArgs:
|
||||
@staticmethod
|
||||
def handle_version():
|
||||
if any((a in ARGS for a in ALLARGS.version)):
|
||||
print(settings.Keys.SWINGMUSIC_APP_VERSION)
|
||||
print(f"VERSION: v{settings.Keys.SWINGMUSIC_APP_VERSION}")
|
||||
print(
|
||||
f"COMMIT#: {settings.Keys.GIT_CURRENT_BRANCH}/{settings.Keys.GIT_LATEST_COMMIT_HASH}"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
SWINGMUSIC_APP_VERSION = ""
|
||||
GIT_LATEST_COMMIT_HASH = ""
|
||||
GIT_CURRENT_BRANCH = ""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
from app.models import FavType
|
||||
from .utils import SQLiteManager
|
||||
|
||||
@@ -26,9 +27,10 @@ class SQLiteFavoriteMethods:
|
||||
if cls.check_is_favorite(fav_hash, fav_type):
|
||||
return
|
||||
|
||||
sql = """INSERT INTO favorites(type, hash) VALUES(?,?)"""
|
||||
sql = """INSERT INTO favorites(type, hash, timestamp) VALUES(?,?,?)"""
|
||||
current_timestamp = datetime.now().timestamp()
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (fav_type, fav_hash))
|
||||
cur.execute(sql, (fav_type, fav_hash, current_timestamp))
|
||||
cur.close()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -66,9 +66,9 @@ class PluginsMethods:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def plugin_set_active(cls, name: str, state: int):
|
||||
def plugin_set_active(cls, name: str, active: int):
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute("UPDATE plugins SET active=? WHERE name=?", (state, name))
|
||||
cur.execute("UPDATE plugins SET active=? WHERE name=?", (active, name))
|
||||
cur.close()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -15,7 +15,8 @@ CREATE TABLE IF NOT EXISTS playlists (
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
id integer PRIMARY KEY,
|
||||
hash text not null,
|
||||
type text not null
|
||||
type text not null,
|
||||
timestamp integer not null default 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.serializers.artist import serialize_for_card
|
||||
|
||||
from itertools import groupby
|
||||
|
||||
from app.utils.dates import timestamp_from_days_ago, timestamp_to_time_passed
|
||||
from app.utils.dates import timestamp_to_time_passed
|
||||
|
||||
older_albums = set()
|
||||
older_artists = set()
|
||||
@@ -216,8 +216,6 @@ def get_recent_items(limit: int = 7):
|
||||
return recent_items
|
||||
|
||||
|
||||
def get_recent_tracks(cutoff_days: int):
|
||||
def get_recent_tracks(limit: int):
|
||||
tracks = sorted(TrackStore.tracks, key=lambda t: t.created_date, reverse=True)
|
||||
timestamp = timestamp_from_days_ago(cutoff_days)
|
||||
|
||||
return [t for t in tracks if t.created_date > timestamp]
|
||||
return tracks[:limit]
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
This library contains all the functions related to playlists.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
@@ -62,7 +63,7 @@ def create_gif_thumbnail(image: Any, img_path: str):
|
||||
|
||||
|
||||
def save_p_image(
|
||||
img: Image, pid: str, content_type: str = None, filename: str = None
|
||||
img: Image, pid: int, content_type: str = None, filename: str = None
|
||||
) -> str:
|
||||
"""
|
||||
Saves a playlist banner image and returns the filepath.
|
||||
@@ -72,7 +73,7 @@ def save_p_image(
|
||||
random_str = "".join(random.choices(string.ascii_letters + string.digits, k=5))
|
||||
|
||||
if not filename:
|
||||
filename = pid + str(random_str) + ".webp"
|
||||
filename = str(pid) + str(random_str) + ".webp"
|
||||
|
||||
full_img_path = os.path.join(settings.Paths.get_playlist_img_path(), filename)
|
||||
|
||||
@@ -134,7 +135,7 @@ def get_first_4_images(
|
||||
return duplicate_images(images)
|
||||
|
||||
|
||||
def get_recently_added_playlist(cutoff: int = 14):
|
||||
def get_recently_added_playlist(limit: int = 100):
|
||||
playlist = Playlist(
|
||||
id="recentlyadded",
|
||||
name="Recently Added",
|
||||
@@ -144,8 +145,10 @@ def get_recently_added_playlist(cutoff: int = 14):
|
||||
trackhashes=[],
|
||||
)
|
||||
|
||||
tracks = get_recent_tracks(cutoff)
|
||||
tracks = get_recent_tracks(limit=limit)
|
||||
|
||||
try:
|
||||
# Create date to show as last updated
|
||||
date = datetime.fromtimestamp(tracks[0].created_date)
|
||||
except IndexError:
|
||||
return playlist, []
|
||||
|
||||
@@ -60,7 +60,7 @@ def get_silence_paddings(ending_file: str, starting_file: str):
|
||||
"""
|
||||
Returns the ending silence of a track and the starting silence of the next.
|
||||
"""
|
||||
silence = {"start": 0, "end": 0}
|
||||
silence = {"starting_file": 0, "ending_file": 0}
|
||||
ending_thread = None
|
||||
starting_thread = None
|
||||
|
||||
@@ -77,9 +77,9 @@ def get_silence_paddings(ending_file: str, starting_file: str):
|
||||
starting_thread.start()
|
||||
|
||||
if ending_thread:
|
||||
silence["end"] = ending_thread.join()
|
||||
silence["ending_file"] = ending_thread.join()
|
||||
|
||||
if starting_thread:
|
||||
silence["start"] = starting_thread.join()
|
||||
silence["starting_file"] = starting_thread.join()
|
||||
|
||||
return silence
|
||||
|
||||
@@ -10,10 +10,9 @@ ONLY MODIFY OLD MIGRATIONS FOR BUG FIXES OR ENHANCEMENTS ONLY
|
||||
PS: Fuck that! Do what you want.
|
||||
"""
|
||||
|
||||
|
||||
from app.db.sqlite.migrations import MigrationManager
|
||||
from app.logger import log
|
||||
from app.migrations import v1_3_0
|
||||
from app.migrations import v1_3_0, v1_4_9
|
||||
from app.migrations.base import Migration
|
||||
|
||||
migrations: list[list[Migration]] = [
|
||||
@@ -26,7 +25,8 @@ migrations: list[list[Migration]] = [
|
||||
v1_3_0.MovePlaylistsAndFavoritesTo10BitHashes,
|
||||
v1_3_0.RemoveAllTracks,
|
||||
v1_3_0.UpdateAppSettingsTable,
|
||||
]
|
||||
],
|
||||
[v1_4_9.AddTimestampToFavoritesTable],
|
||||
]
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ def apply_migrations():
|
||||
version = MigrationManager.get_version()
|
||||
|
||||
if version != len(migrations):
|
||||
# run migrations after the previous migration version
|
||||
for migration in migrations[(version - 1) :]:
|
||||
# INFO: Apply new migrations
|
||||
for migration in migrations[version:]:
|
||||
for m in migration:
|
||||
try:
|
||||
m.migrate()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from app.db.sqlite.utils import SQLiteManager
|
||||
from app.migrations.base import Migration
|
||||
|
||||
|
||||
class AddTimestampToFavoritesTable(Migration):
|
||||
"""
|
||||
Adds a timestamp column to the favorites table.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def migrate():
|
||||
# INFO: add timestamp column with automatic current timestamp
|
||||
sql = f"ALTER TABLE favorites ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0"
|
||||
|
||||
# INFO: execute the sql
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql)
|
||||
|
||||
# INFO: Update the timestamp column with the current timestamp
|
||||
cur.execute("UPDATE favorites SET timestamp = strftime('%s', 'now')")
|
||||
cur.close()
|
||||
|
||||
|
||||
class MoveHashesToSha1(Migration):
|
||||
"""
|
||||
Moves the 10 bit item hashes from sha256 to sha1 which is
|
||||
faster and more lenient on less powerful devices.
|
||||
|
||||
Thanks to [@tcsenpai](https:github.com/tcsenpai) for the contribution.
|
||||
"""
|
||||
pass
|
||||
|
||||
# INFO: Apparentlly, every single table is affected by this migration.
|
||||
# NOTE: Use generators to avoid memory issues.
|
||||
+42
-3
@@ -1,7 +1,9 @@
|
||||
"""
|
||||
Contains default configs
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
@@ -96,6 +98,15 @@ class Defaults:
|
||||
"""
|
||||
The size of extracted images in pixels
|
||||
"""
|
||||
HASH_LENGTH = 10
|
||||
API_ALBUMHASH = "c5bcec6cb3"
|
||||
API_ARTISTHASH = "fc6f0acac5"
|
||||
API_TRACKHASH = "0853280a12"
|
||||
API_ALBUMNAME = "Rumours"
|
||||
API_ARTISTNAME = "girl in red"
|
||||
API_TRACKNAME = "Apartment 402"
|
||||
|
||||
API_CARD_LIMIT = 6
|
||||
|
||||
|
||||
FILES = ["flac", "mp3", "wav", "m4a", "ogg", "wma", "opus", "alac", "aiff"]
|
||||
@@ -233,21 +244,49 @@ class TCOLOR:
|
||||
# credits: https://stackoverflow.com/a/287944
|
||||
|
||||
|
||||
def getLatestCommitHash():
|
||||
"""
|
||||
Returns the latest git commit hash for the current branch
|
||||
"""
|
||||
|
||||
try:
|
||||
hash = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
|
||||
return hash.decode("utf-8").strip()
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
def getCurrentBranch():
|
||||
"""
|
||||
Returns the current git branch
|
||||
"""
|
||||
|
||||
try:
|
||||
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
return branch.decode("utf-8").strip()
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
class Keys:
|
||||
SWINGMUSIC_APP_VERSION = os.environ.get("SWINGMUSIC_APP_VERSION")
|
||||
GIT_LATEST_COMMIT_HASH = "<unset>"
|
||||
GIT_CURRENT_BRANCH = "<unset>"
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
if IS_BUILD:
|
||||
cls.SWINGMUSIC_APP_VERSION = configs.SWINGMUSIC_APP_VERSION
|
||||
cls.GIT_LATEST_COMMIT_HASH = configs.GIT_LATEST_COMMIT_HASH
|
||||
cls.GIT_CURRENT_BRANCH = configs.GIT_CURRENT_BRANCH
|
||||
else:
|
||||
cls.GIT_LATEST_COMMIT_HASH = getLatestCommitHash()
|
||||
cls.GIT_CURRENT_BRANCH = getCurrentBranch()
|
||||
|
||||
cls.verify_keys()
|
||||
|
||||
@classmethod
|
||||
def verify_keys(cls):
|
||||
# if not cls.LASTFM_API_KEY:
|
||||
# print("ERROR: LASTFM_API_KEY not set in environment")
|
||||
# sys.exit(0)
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
from app.db.sqlite.artistcolors import SQLiteArtistMethods as ardb
|
||||
from app.lib.artistlib import get_all_artists
|
||||
from app.models import Artist
|
||||
from app.utils.bisection import UseBisection
|
||||
from app.utils.bisection import use_bisection
|
||||
from app.utils.customlist import CustomList
|
||||
from app.utils.progressbar import tqdm
|
||||
|
||||
@@ -72,7 +72,7 @@ class ArtistStore:
|
||||
"""
|
||||
artists = sorted(cls.artists, key=lambda x: x.artisthash)
|
||||
try:
|
||||
artist = UseBisection(artists, "artisthash", [artisthash])()[0]
|
||||
artist = use_bisection(artists, "artisthash", [artisthash])[0]
|
||||
return artist
|
||||
except IndexError:
|
||||
return None
|
||||
@@ -83,7 +83,7 @@ class ArtistStore:
|
||||
Returns artists by their hashes.
|
||||
"""
|
||||
artists = sorted(cls.artists, key=lambda x: x.artisthash)
|
||||
artists = UseBisection(artists, "artisthash", artisthashes)()
|
||||
artists = use_bisection(artists, "artisthash", artisthashes)
|
||||
return [a for a in artists if a is not None]
|
||||
|
||||
@classmethod
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.db.sqlite.tracks import SQLiteTrackMethods as tdb
|
||||
from app.models import Track
|
||||
from app.utils.bisection import UseBisection
|
||||
from app.utils.bisection import use_bisection
|
||||
from app.utils.customlist import CustomList
|
||||
from app.utils.remove_duplicates import remove_duplicates
|
||||
|
||||
@@ -153,7 +153,7 @@ class TrackStore:
|
||||
Returns all tracks matching the given paths.
|
||||
"""
|
||||
tracks = sorted(cls.tracks, key=lambda x: x.filepath)
|
||||
tracks = UseBisection(tracks, "filepath", paths)()
|
||||
tracks = use_bisection(tracks, "filepath", paths)
|
||||
return [track for track in tracks if track is not None]
|
||||
|
||||
@classmethod
|
||||
|
||||
+16
-23
@@ -1,52 +1,45 @@
|
||||
from app.models.track import Track
|
||||
from typing import List, Optional, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class UseBisection:
|
||||
def use_bisection(
|
||||
source: List[T], key: str, queries: List[str], limit: int = -1
|
||||
) -> List[Optional[T]]:
|
||||
"""
|
||||
Uses bisection to find a list of items in another list.
|
||||
|
||||
returns a list of found items with `None` items being not found
|
||||
items.
|
||||
Returns a list of found items with `None` items being not found items.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, source: list, search_from: str, queries: list[str], limit=-1
|
||||
) -> None:
|
||||
self.source_list = source
|
||||
self.queries_list = queries
|
||||
self.attr = search_from
|
||||
self.limit = limit
|
||||
|
||||
def find(self, query: str):
|
||||
def find(query: str):
|
||||
left = 0
|
||||
right = len(self.source_list) - 1
|
||||
right = len(source) - 1
|
||||
|
||||
while left <= right:
|
||||
mid = (left + right) // 2
|
||||
if self.source_list[mid].__getattribute__(self.attr) == query:
|
||||
return self.source_list[mid]
|
||||
elif self.source_list[mid].__getattribute__(self.attr) > query:
|
||||
if source[mid].__getattribute__(key) == query:
|
||||
return source[mid]
|
||||
elif source[mid].__getattribute__(key) > query:
|
||||
right = mid - 1
|
||||
else:
|
||||
left = mid + 1
|
||||
|
||||
return None
|
||||
|
||||
def __call__(self):
|
||||
if len(self.source_list) == 0:
|
||||
if len(source) == 0:
|
||||
return []
|
||||
|
||||
results: list[Track] = []
|
||||
results = []
|
||||
|
||||
for query in self.queries_list:
|
||||
res = self.find(query)
|
||||
for query in queries:
|
||||
res = find(query)
|
||||
|
||||
if res is None:
|
||||
continue
|
||||
|
||||
results.append(res)
|
||||
|
||||
if self.limit != -1 and len(results) >= self.limit:
|
||||
if limit != -1 and len(results) >= limit:
|
||||
break
|
||||
|
||||
return results
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import sys
|
||||
|
||||
|
||||
def getFlaskOpenApiPath():
|
||||
"""
|
||||
Used to retrieve the path to the flask_openapi3 package
|
||||
|
||||
See: https://github.com/luolingchun/flask-openapi3/issues/147
|
||||
"""
|
||||
site_packages_path = [p for p in sys.path if "site-packages" in p][0]
|
||||
|
||||
return f"{site_packages_path}/flask_openapi3"
|
||||
Generated
+129
-32
@@ -12,19 +12,16 @@ files = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aniso8601"
|
||||
version = "9.0.1"
|
||||
description = "A library for parsing ISO 8601 strings."
|
||||
name = "annotated-types"
|
||||
version = "0.6.0"
|
||||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"},
|
||||
{file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"},
|
||||
{file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
|
||||
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"]
|
||||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "2.15.8"
|
||||
@@ -561,24 +558,25 @@ Flask = ">=0.9"
|
||||
Six = "*"
|
||||
|
||||
[[package]]
|
||||
name = "flask-restful"
|
||||
version = "0.3.10"
|
||||
description = "Simple framework for creating REST APIs"
|
||||
name = "flask-openapi3"
|
||||
version = "3.0.2"
|
||||
description = "Generate REST API and OpenAPI documentation for your Flask project."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Flask-RESTful-0.3.10.tar.gz", hash = "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37"},
|
||||
{file = "Flask_RESTful-0.3.10-py2.py3-none-any.whl", hash = "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b"},
|
||||
{file = "flask_openapi3-3.0.2-py3-none-any.whl", hash = "sha256:e39359bf07da47d9abf30d4b370feaac4035bd805f03518ce65016958796c636"},
|
||||
{file = "flask_openapi3-3.0.2.tar.gz", hash = "sha256:92e6a308c5b13692ad8aea04ee951ffbfabc6853b20bd3b678c2569dcb781916"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aniso8601 = ">=0.82"
|
||||
Flask = ">=0.8"
|
||||
pytz = "*"
|
||||
six = ">=1.3.0"
|
||||
flask = ">=2.0"
|
||||
pydantic = ">=2.4"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx"]
|
||||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
email = ["email-validator"]
|
||||
yaml = ["pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "gevent"
|
||||
@@ -1447,6 +1445,116 @@ files = [
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.6.3"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"},
|
||||
{file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.4.0"
|
||||
pydantic-core = "2.16.3"
|
||||
typing-extensions = ">=4.6.1"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.16.3"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"},
|
||||
{file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"},
|
||||
{file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"},
|
||||
{file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"},
|
||||
{file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"},
|
||||
{file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"},
|
||||
{file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"},
|
||||
{file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"},
|
||||
{file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"},
|
||||
{file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"},
|
||||
{file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"},
|
||||
{file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"},
|
||||
{file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"},
|
||||
{file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"},
|
||||
{file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"},
|
||||
{file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"},
|
||||
{file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"},
|
||||
{file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"},
|
||||
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"},
|
||||
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"},
|
||||
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"},
|
||||
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"},
|
||||
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"},
|
||||
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"},
|
||||
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"},
|
||||
{file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"},
|
||||
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"},
|
||||
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"},
|
||||
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"},
|
||||
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"},
|
||||
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"},
|
||||
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"},
|
||||
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"},
|
||||
{file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"},
|
||||
{file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "5.13.2"
|
||||
@@ -1555,17 +1663,6 @@ files = [
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2023.3.post1"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"},
|
||||
{file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "306"
|
||||
@@ -2371,4 +2468,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.12"
|
||||
content-hash = "a4c52a13b0952ab8848381800032d35994c17d39ad6f884f509c7f6d848a5ce9"
|
||||
content-hash = "feb13f92b7b3a909fcb851860a405b96579feac0e2dde7681ed0e9c381c4f6cd"
|
||||
|
||||
+1
-1
@@ -20,11 +20,11 @@ show-in-file-manager = "^1.1.4"
|
||||
flask-compress = "^1.13"
|
||||
tabulate = "^0.9.0"
|
||||
setproctitle = "^1.3.2"
|
||||
flask-restful = "^0.3.10"
|
||||
locust = "^2.20.1"
|
||||
waitress = "^2.1.2"
|
||||
watchdog = "^4.0.0"
|
||||
pendulum = "^3.0.0"
|
||||
flask-openapi3 = "^3.0.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pylint = "^2.15.5"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
altgraph==0.17.4
|
||||
annotated-types==0.6.0
|
||||
astroid==2.15.8
|
||||
attrs==23.1.0
|
||||
black==22.12.0
|
||||
blinker==1.6.3
|
||||
Brotli==1.1.0
|
||||
certifi==2023.7.22
|
||||
charset-normalizer==3.3.0
|
||||
click==8.1.7
|
||||
colorgram.py==1.2.0
|
||||
ConfigArgParse==1.7
|
||||
dill==0.3.7
|
||||
Flask==2.3.3
|
||||
Flask-BasicAuth==0.2.0
|
||||
Flask-Compress==1.14
|
||||
Flask-Cors==3.0.10
|
||||
flask-openapi3==3.0.2
|
||||
gevent==23.9.1
|
||||
geventhttpclient==2.0.11
|
||||
greenlet==3.0.3
|
||||
hypothesis==6.87.3
|
||||
idna==3.4
|
||||
iniconfig==2.0.0
|
||||
isort==5.12.0
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
lazy-object-proxy==1.9.0
|
||||
locust==2.20.1
|
||||
MarkupSafe==2.1.3
|
||||
mccabe==0.7.0
|
||||
msgpack==1.0.7
|
||||
mypy-extensions==1.0.0
|
||||
packaging==23.2
|
||||
pathspec==0.11.2
|
||||
pendulum==3.0.0
|
||||
Pillow==9.5.0
|
||||
platformdirs==3.11.0
|
||||
pluggy==1.3.0
|
||||
psutil==5.9.5
|
||||
pydantic==2.6.3
|
||||
pydantic_core==2.16.3
|
||||
pyinstaller==5.13.2
|
||||
pyinstaller-hooks-contrib==2023.9
|
||||
pylint==2.17.7
|
||||
pytest==7.4.2
|
||||
python-dateutil==2.8.2
|
||||
pyxdg==0.28
|
||||
pyzmq==25.1.2
|
||||
rapidfuzz==2.15.2
|
||||
requests==2.31.0
|
||||
roundrobin==0.0.4
|
||||
setproctitle==1.3.3
|
||||
show-in-file-manager==1.1.4
|
||||
six==1.16.0
|
||||
sortedcontainers==2.4.0
|
||||
tabulate==0.9.0
|
||||
tinytag==1.9.0
|
||||
tomlkit==0.12.1
|
||||
tqdm==4.66.1
|
||||
typing_extensions==4.8.0
|
||||
tzdata==2024.1
|
||||
Unidecode==1.3.7
|
||||
urllib3==2.0.6
|
||||
waitress==2.1.2
|
||||
watchdog==4.0.0
|
||||
Werkzeug==3.0.0
|
||||
wrapt==1.15.0
|
||||
zope.event==5.0
|
||||
zope.interface==6.1
|
||||
Reference in New Issue
Block a user