mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
This module combines all API blueprints into a single Flask app instance.
|
||||
"""
|
||||
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
|
||||
from app.api import album, artist, favorites, folder, playlist, search, track
|
||||
from app.imgserver import imgbp as imgserver
|
||||
|
||||
|
||||
def create_api():
|
||||
"""
|
||||
Creates the Flask instance, registers modules and registers all the API blueprints.
|
||||
"""
|
||||
app = Flask(__name__, static_url_path="")
|
||||
CORS(app)
|
||||
|
||||
with app.app_context():
|
||||
|
||||
app.register_blueprint(album.albumbp)
|
||||
app.register_blueprint(artist.artistbp)
|
||||
app.register_blueprint(track.trackbp)
|
||||
app.register_blueprint(search.searchbp)
|
||||
app.register_blueprint(folder.folderbp)
|
||||
app.register_blueprint(playlist.playlistbp)
|
||||
app.register_blueprint(favorites.favbp)
|
||||
app.register_blueprint(imgserver)
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Contains all the album routes.
|
||||
"""
|
||||
|
||||
from dataclasses import asdict
|
||||
|
||||
from flask import Blueprint, request
|
||||
|
||||
from app import utils
|
||||
from app.db.sqlite.albums import SQLiteAlbumMethods as adb
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.db.store import Store
|
||||
from app.models import FavType, Track
|
||||
|
||||
|
||||
get_album_by_id = adb.get_album_by_id
|
||||
get_albums_by_albumartist = adb.get_albums_by_albumartist
|
||||
check_is_fav = favdb.check_is_favorite
|
||||
|
||||
albumbp = Blueprint("album", __name__, url_prefix="")
|
||||
|
||||
|
||||
@albumbp.route("/album", methods=["POST"])
|
||||
def get_album():
|
||||
"""Returns all the tracks in the given album."""
|
||||
|
||||
data = request.get_json()
|
||||
error_msg = {"msg": "No hash provided"}
|
||||
|
||||
if data is None:
|
||||
return error_msg, 400
|
||||
|
||||
try:
|
||||
albumhash = data["hash"]
|
||||
except KeyError:
|
||||
return error_msg, 400
|
||||
|
||||
error_msg = {"error": "Album not created yet."}
|
||||
album = Store.get_album_by_hash(albumhash)
|
||||
|
||||
if album is None:
|
||||
return error_msg, 204
|
||||
|
||||
tracks = Store.get_tracks_by_albumhash(albumhash)
|
||||
|
||||
if tracks is None:
|
||||
return error_msg, 404
|
||||
|
||||
if len(tracks) == 0:
|
||||
return error_msg, 204
|
||||
|
||||
def get_album_genres(tracks: list[Track]):
|
||||
genres = set()
|
||||
|
||||
for track in tracks:
|
||||
if track.genre is not None:
|
||||
genres.update(track.genre)
|
||||
|
||||
return list(genres)
|
||||
|
||||
album.genres = get_album_genres(tracks)
|
||||
tracks = utils.remove_duplicates(tracks)
|
||||
|
||||
album.count = len(tracks)
|
||||
|
||||
for track in tracks:
|
||||
if track.date != "Unknown":
|
||||
album.date = track.date
|
||||
break
|
||||
|
||||
try:
|
||||
album.duration = sum((t.duration for t in tracks))
|
||||
except AttributeError:
|
||||
album.duration = 0
|
||||
|
||||
if (
|
||||
album.count == 1
|
||||
and tracks[0].title == album.title
|
||||
# and tracks[0].track == 1
|
||||
# and tracks[0].disc == 1
|
||||
):
|
||||
album.is_single = True
|
||||
else:
|
||||
album.check_type()
|
||||
|
||||
album.is_favorite = check_is_fav(albumhash, FavType.album)
|
||||
|
||||
return {"tracks": tracks, "info": album}
|
||||
|
||||
|
||||
@albumbp.route("/album/<albumhash>/tracks", methods=["GET"])
|
||||
def get_album_tracks(albumhash: str):
|
||||
"""
|
||||
Returns all the tracks in the given album.
|
||||
"""
|
||||
tracks = Store.get_tracks_by_albumhash(albumhash)
|
||||
tracks = [asdict(t) for t in tracks]
|
||||
|
||||
for t in tracks:
|
||||
track = str(t["track"]).zfill(3)
|
||||
t["pos"] = int(f"{t['disc']}{track}")
|
||||
|
||||
tracks = sorted(tracks, key=lambda t: t["pos"])
|
||||
|
||||
return {"tracks": tracks}
|
||||
|
||||
|
||||
@albumbp.route("/album/from-artist", methods=["POST"])
|
||||
def get_artist_albums():
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return {"msg": "No albumartist provided"}
|
||||
|
||||
albumartists: str = data["albumartists"] # type: ignore
|
||||
limit: int = data.get("limit")
|
||||
exclude: str = data.get("exclude")
|
||||
|
||||
albumartists: list[str] = albumartists.split(",") # type: ignore
|
||||
|
||||
albums = [
|
||||
{
|
||||
"artisthash": a,
|
||||
"albums": Store.get_albums_by_albumartist(a, limit, exclude=exclude),
|
||||
}
|
||||
for a in albumartists
|
||||
]
|
||||
|
||||
albums = [a for a in albums if len(a["albums"]) > 0]
|
||||
|
||||
return {"data": albums}
|
||||
|
||||
|
||||
# @album_bp.route("/album/bio", methods=["POST"])
|
||||
# def get_album_bio():
|
||||
# """Returns the album bio for the given album."""
|
||||
# data = request.get_json()
|
||||
# album_hash = data["hash"]
|
||||
# err_msg = {"bio": "No bio found"}
|
||||
|
||||
# album = instances.album_instance.find_album_by_hash(album_hash)
|
||||
|
||||
# if album is None:
|
||||
# return err_msg, 404
|
||||
|
||||
# bio = FetchAlbumBio(album["title"], album["artist"])()
|
||||
|
||||
# if bio is None:
|
||||
# return err_msg, 404
|
||||
|
||||
# return {"bio": bio}
|
||||
@@ -0,0 +1,323 @@
|
||||
"""
|
||||
Contains all the artist(s) routes.
|
||||
"""
|
||||
from collections import deque
|
||||
|
||||
from flask import Blueprint, request
|
||||
|
||||
from app.db.store import Store
|
||||
from app.models import Album, FavType, Track
|
||||
from app.utils import remove_duplicates
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
|
||||
artistbp = Blueprint("artist", __name__, url_prefix="/")
|
||||
|
||||
|
||||
class CacheEntry:
|
||||
"""
|
||||
The cache entry class for the artists cache.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, artisthash: str, albumhashes: set[str], tracks: list[Track]
|
||||
) -> None:
|
||||
self.albums: list[Album] = []
|
||||
self.tracks: list[Track] = []
|
||||
|
||||
self.artisthash: str = artisthash
|
||||
self.albumhashes: set[str] = albumhashes
|
||||
|
||||
if len(tracks) > 0:
|
||||
self.tracks: list[Track] = tracks
|
||||
|
||||
self.type_checked = False
|
||||
self.albums_fetched = False
|
||||
|
||||
|
||||
class ArtistsCache:
|
||||
"""
|
||||
Holds artist page cache.
|
||||
"""
|
||||
|
||||
artists: deque[CacheEntry] = deque(maxlen=6)
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_artisthash(cls, artisthash: str):
|
||||
"""
|
||||
Returns the cached albums for the given artisthash.
|
||||
"""
|
||||
for (index, albums) in enumerate(cls.artists):
|
||||
if albums.artisthash == artisthash:
|
||||
return (albums.albums, index)
|
||||
|
||||
return ([], -1)
|
||||
|
||||
@classmethod
|
||||
def albums_cached(cls, artisthash: str) -> bool:
|
||||
"""
|
||||
Returns True if the artist is in the cache.
|
||||
"""
|
||||
for entry in cls.artists:
|
||||
if entry.artisthash == artisthash and len(entry.albums) > 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def albums_fetched(cls, artisthash: str):
|
||||
"""
|
||||
Checks if the albums have been fetched for the given artisthash.
|
||||
"""
|
||||
for entry in cls.artists:
|
||||
if entry.artisthash == artisthash:
|
||||
return entry.albums_fetched
|
||||
|
||||
@classmethod
|
||||
def tracks_cached(cls, artisthash: str) -> bool:
|
||||
"""
|
||||
Checks if the tracks have been cached for the given artisthash.
|
||||
"""
|
||||
for entry in cls.artists:
|
||||
if entry.artisthash == artisthash and len(entry.tracks) > 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def add_entry(cls, artisthash: str, albumhashes: set[str], tracks: list[Track]):
|
||||
"""
|
||||
Adds a new entry to the cache.
|
||||
"""
|
||||
cls.artists.append(CacheEntry(artisthash, albumhashes, tracks))
|
||||
|
||||
@classmethod
|
||||
def get_tracks(cls, artisthash: str):
|
||||
"""
|
||||
Returns the cached tracks for the given artisthash.
|
||||
"""
|
||||
entry = [a for a in cls.artists if a.artisthash == artisthash][0]
|
||||
return entry.tracks
|
||||
|
||||
@classmethod
|
||||
def get_albums(cls, artisthash: str):
|
||||
"""
|
||||
Returns the cached albums for the given artisthash.
|
||||
"""
|
||||
entry = [a for a in cls.artists if a.artisthash == artisthash][0]
|
||||
|
||||
albums = [Store.get_album_by_hash(h) for h in entry.albumhashes]
|
||||
entry.albums = [album for album in albums if album is not None]
|
||||
|
||||
store_albums = Store.get_albums_by_artisthash(artisthash)
|
||||
|
||||
all_albums_hash = "-".join([a.albumhash for a in entry.albums])
|
||||
|
||||
for album in store_albums:
|
||||
if album.albumhash not in all_albums_hash:
|
||||
entry.albums.append(album)
|
||||
|
||||
entry.albums_fetched = True
|
||||
|
||||
@classmethod
|
||||
def process_album_type(cls, artisthash: str):
|
||||
"""
|
||||
Checks the cached albums type for the given artisthash.
|
||||
"""
|
||||
entry = [a for a in cls.artists if a.artisthash == artisthash][0]
|
||||
|
||||
for album in entry.albums:
|
||||
album.check_type()
|
||||
|
||||
album_tracks = Store.get_tracks_by_albumhash(album.albumhash)
|
||||
album_tracks = remove_duplicates(album_tracks)
|
||||
|
||||
album.check_is_single(album_tracks)
|
||||
|
||||
entry.type_checked = True
|
||||
|
||||
|
||||
def add_albums_to_cache(artisthash: str):
|
||||
"""
|
||||
Fetches albums and adds them to the cache.
|
||||
"""
|
||||
tracks = Store.get_tracks_by_artist(artisthash)
|
||||
|
||||
if len(tracks) == 0:
|
||||
return False
|
||||
|
||||
albumhashes = set(t.albumhash for t in tracks)
|
||||
ArtistsCache.add_entry(artisthash, albumhashes, [])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# =======================================================
|
||||
# ===================== ROUTES ==========================
|
||||
# =======================================================
|
||||
|
||||
|
||||
@artistbp.route("/artist/<artisthash>", methods=["GET"])
|
||||
def get_artist(artisthash: str):
|
||||
"""
|
||||
Get artist data.
|
||||
"""
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
artist = Store.get_artist_by_hash(artisthash)
|
||||
|
||||
if artist is None:
|
||||
return {"error": "Artist not found"}, 404
|
||||
|
||||
tracks_cached = ArtistsCache.tracks_cached(artisthash)
|
||||
|
||||
if tracks_cached:
|
||||
tracks = ArtistsCache.get_tracks(artisthash)
|
||||
else:
|
||||
tracks = Store.get_tracks_by_artist(artisthash)
|
||||
albumhashes = set(t.albumhash for t in tracks)
|
||||
hashes_from_albums = set(
|
||||
a.albumhash for a in Store.get_albums_by_artisthash(artisthash)
|
||||
)
|
||||
|
||||
albumhashes = albumhashes.union(hashes_from_albums)
|
||||
ArtistsCache.add_entry(artisthash, albumhashes, tracks)
|
||||
|
||||
tcount = len(tracks)
|
||||
acount = Store.count_albums_by_artisthash(artisthash)
|
||||
|
||||
if acount == 0 and tcount < 10:
|
||||
limit = tcount
|
||||
|
||||
artist.trackcount = tcount
|
||||
artist.albumcount = acount
|
||||
|
||||
artist.duration = sum(t.duration for t in tracks)
|
||||
|
||||
artist.is_favorite = favdb.check_is_favorite(artisthash, FavType.artist)
|
||||
|
||||
return {"artist": artist, "tracks": tracks[:limit]}
|
||||
|
||||
|
||||
@artistbp.route("/artist/<artisthash>/albums", methods=["GET"])
|
||||
def get_artist_albums(artisthash: str):
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
return_all = request.args.get("all")
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
all_albums = []
|
||||
is_cached = ArtistsCache.albums_cached(artisthash)
|
||||
|
||||
if not is_cached:
|
||||
add_albums_to_cache(artisthash)
|
||||
|
||||
albums_fetched = ArtistsCache.albums_fetched(artisthash)
|
||||
|
||||
if not albums_fetched:
|
||||
ArtistsCache.get_albums(artisthash)
|
||||
|
||||
all_albums, index = ArtistsCache.get_albums_by_artisthash(artisthash)
|
||||
|
||||
if not ArtistsCache.artists[index].type_checked:
|
||||
ArtistsCache.process_album_type(artisthash)
|
||||
|
||||
singles = [a for a in all_albums if a.is_single]
|
||||
eps = [a for a in all_albums if a.is_EP]
|
||||
|
||||
def remove_EPs_and_singles(albums: list[Album]):
|
||||
albums = [a for a in albums if not a.is_EP]
|
||||
albums = [a for a in albums if not a.is_single]
|
||||
return albums
|
||||
|
||||
albums = filter(lambda a: artisthash in a.albumartisthash, all_albums)
|
||||
albums = list(albums)
|
||||
albums = remove_EPs_and_singles(albums)
|
||||
|
||||
appearances = filter(lambda a: artisthash not in a.albumartisthash, all_albums)
|
||||
appearances = list(appearances)
|
||||
|
||||
appearances = remove_EPs_and_singles(appearances)
|
||||
|
||||
artist = Store.get_artist_by_hash(artisthash)
|
||||
|
||||
if return_all is not None:
|
||||
limit = len(all_albums)
|
||||
|
||||
return {
|
||||
"artistname": artist.name,
|
||||
"albums": albums[:limit],
|
||||
"singles": singles[:limit],
|
||||
"eps": eps[:limit],
|
||||
"appearances": appearances[:limit],
|
||||
}
|
||||
|
||||
|
||||
@artistbp.route("/artist/<artisthash>/tracks", methods=["GET"])
|
||||
def get_artist_tracks(artisthash: str):
|
||||
"""
|
||||
Returns all artists by a given artist.
|
||||
"""
|
||||
tracks = Store.get_tracks_by_artist(artisthash)
|
||||
|
||||
return {"tracks": tracks}
|
||||
# artist = Store.get_artist_by_hash(artisthash)
|
||||
# if artist is None:
|
||||
# return {"error": "Artist not found"}, 404
|
||||
|
||||
# return {"albums": albums[:limit]}
|
||||
|
||||
|
||||
# @artist_bp.route("/artist/<artist>")
|
||||
# @cache.cached()
|
||||
# def get_artist_data(artist: str):
|
||||
# """Returns the artist's data, tracks and albums"""
|
||||
# artist = urllib.parse.unquote(artist)
|
||||
# artist_obj = instances.artist_instance.get_artists_by_name(artist)
|
||||
|
||||
# def get_artist_tracks():
|
||||
# songs = instances.tracks_instance.find_songs_by_artist(artist)
|
||||
|
||||
# return songs
|
||||
|
||||
# artist_songs = get_artist_tracks()
|
||||
# songs = utils.remove_duplicates(artist_songs)
|
||||
|
||||
# def get_artist_albums():
|
||||
# artist_albums = []
|
||||
# albums_with_count = []
|
||||
|
||||
# albums = instances.tracks_instance.find_songs_by_albumartist(artist)
|
||||
|
||||
# for song in albums:
|
||||
# if song["album"] not in artist_albums:
|
||||
# artist_albums.append(song["album"])
|
||||
|
||||
# for album in artist_albums:
|
||||
# count = 0
|
||||
# length = 0
|
||||
|
||||
# for song in artist_songs:
|
||||
# if song["album"] == album:
|
||||
# count = count + 1
|
||||
# length = length + song["length"]
|
||||
|
||||
# album_ = {"title": album, "count": count, "length": length}
|
||||
|
||||
# albums_with_count.append(album_)
|
||||
|
||||
# return albums_with_count
|
||||
|
||||
# return {
|
||||
# "artist": artist_obj,
|
||||
# "songs": songs,
|
||||
# "albums": get_artist_albums()
|
||||
# }
|
||||
@@ -0,0 +1,210 @@
|
||||
from flask import Blueprint, request
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.db.store import Store
|
||||
from app.models import FavType
|
||||
from app.utils import UseBisection
|
||||
|
||||
favbp = Blueprint("favorite", __name__, url_prefix="/")
|
||||
|
||||
|
||||
def remove_none(items: list):
|
||||
return [i for i in items if i is not None]
|
||||
|
||||
|
||||
@favbp.route("/favorite/add", methods=["POST"])
|
||||
def add_favorite():
|
||||
"""
|
||||
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")
|
||||
|
||||
favdb.insert_one_favorite(itemtype, itemhash)
|
||||
|
||||
if itemtype == FavType.track:
|
||||
Store.add_fav_track(itemhash)
|
||||
|
||||
return {"msg": "Added to favorites"}
|
||||
|
||||
|
||||
@favbp.route("/favorite/remove", methods=["POST"])
|
||||
def remove_favorite():
|
||||
"""
|
||||
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")
|
||||
|
||||
favdb.delete_favorite(itemtype, itemhash)
|
||||
|
||||
if itemtype == FavType.track:
|
||||
Store.remove_fav_track(itemhash)
|
||||
|
||||
return {"msg": "Removed from favorites"}
|
||||
|
||||
|
||||
@favbp.route("/albums/favorite")
|
||||
def get_favorite_albums():
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
albums = favdb.get_fav_albums()
|
||||
albumhashes = [a[1] for a in albums]
|
||||
albumhashes.reverse()
|
||||
|
||||
src_albums = sorted(Store.albums, key=lambda x: x.albumhash)
|
||||
|
||||
fav_albums = UseBisection(src_albums, "albumhash", albumhashes)()
|
||||
fav_albums = remove_none(fav_albums)
|
||||
|
||||
if limit == 0:
|
||||
limit = len(albums)
|
||||
|
||||
return {"albums": fav_albums[:limit]}
|
||||
|
||||
|
||||
@favbp.route("/tracks/favorite")
|
||||
def get_favorite_tracks():
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
tracks = favdb.get_fav_tracks()
|
||||
trackhashes = [t[1] for t in tracks]
|
||||
trackhashes.reverse()
|
||||
src_tracks = sorted(Store.tracks, key=lambda x: x.trackhash)
|
||||
|
||||
tracks = UseBisection(src_tracks, "trackhash", trackhashes)()
|
||||
tracks = remove_none(tracks)
|
||||
|
||||
if limit == 0:
|
||||
limit = len(tracks)
|
||||
|
||||
return {"tracks": tracks[:limit]}
|
||||
|
||||
|
||||
@favbp.route("/artists/favorite")
|
||||
def get_favorite_artists():
|
||||
limit = request.args.get("limit")
|
||||
|
||||
if limit is None:
|
||||
limit = 6
|
||||
|
||||
limit = int(limit)
|
||||
|
||||
artists = favdb.get_fav_artists()
|
||||
artisthashes = [a[1] for a in artists]
|
||||
artisthashes.reverse()
|
||||
|
||||
src_artists = sorted(Store.artists, key=lambda x: x.artisthash)
|
||||
|
||||
artists = UseBisection(src_artists, "artisthash", artisthashes)()
|
||||
artists = remove_none(artists)
|
||||
|
||||
if limit == 0:
|
||||
limit = len(artists)
|
||||
|
||||
return {"artists": artists[:limit]}
|
||||
|
||||
|
||||
@favbp.route("/favorites")
|
||||
def get_all_favorites():
|
||||
"""
|
||||
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)
|
||||
|
||||
favs = favdb.get_all()
|
||||
favs.reverse()
|
||||
|
||||
tracks = []
|
||||
albums = []
|
||||
artists = []
|
||||
|
||||
for fav in favs:
|
||||
if (
|
||||
len(tracks) >= track_limit
|
||||
and len(albums) >= album_limit
|
||||
and len(artists) >= artist_limit
|
||||
):
|
||||
break
|
||||
|
||||
if fav[2] == FavType.track:
|
||||
tracks.append(fav[1])
|
||||
elif fav[2] == FavType.album:
|
||||
albums.append(fav[1])
|
||||
elif fav[2] == FavType.artist:
|
||||
artists.append(fav[1])
|
||||
|
||||
src_tracks = sorted(Store.tracks, key=lambda x: x.trackhash)
|
||||
src_albums = sorted(Store.albums, key=lambda x: x.albumhash)
|
||||
src_artists = sorted(Store.artists, key=lambda x: x.artisthash)
|
||||
|
||||
tracks = tracks[:track_limit]
|
||||
albums = albums[:album_limit]
|
||||
artists = artists[:artist_limit]
|
||||
|
||||
tracks = UseBisection(src_tracks, "trackhash", tracks)()
|
||||
albums = UseBisection(src_albums, "albumhash", albums)()
|
||||
artists = UseBisection(src_artists, "artisthash", artists)()
|
||||
|
||||
tracks = remove_none(tracks)
|
||||
albums = remove_none(albums)
|
||||
artists = remove_none(artists)
|
||||
|
||||
return {
|
||||
"tracks": tracks,
|
||||
"albums": albums,
|
||||
"artists": artists,
|
||||
}
|
||||
|
||||
|
||||
@favbp.route("/favorites/check")
|
||||
def check_favorite():
|
||||
"""
|
||||
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
|
||||
|
||||
exists = favdb.check_is_favorite(itemhash, itemtype)
|
||||
|
||||
return {"is_favorite": exists}
|
||||
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Contains all the folder routes.
|
||||
"""
|
||||
from flask import Blueprint, request
|
||||
|
||||
from app import settings
|
||||
from app.lib.folderslib import GetFilesAndDirs
|
||||
|
||||
folderbp = Blueprint("folder", __name__, url_prefix="/")
|
||||
|
||||
|
||||
@folderbp.route("/folder", methods=["POST"])
|
||||
def get_folder_tree():
|
||||
"""
|
||||
Returns a list of all the folders and tracks in the given folder.
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if data is not None:
|
||||
req_dir: str = data["folder"]
|
||||
else:
|
||||
req_dir = settings.HOME_DIR
|
||||
|
||||
if req_dir == "$home":
|
||||
req_dir = settings.HOME_DIR
|
||||
|
||||
tracks, folders = GetFilesAndDirs(req_dir)()
|
||||
|
||||
return {
|
||||
"tracks": tracks,
|
||||
"folders": sorted(folders, key=lambda i: i.name),
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
All playlist-related routes.
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, request
|
||||
from PIL import UnidentifiedImageError
|
||||
|
||||
from app import models, serializer
|
||||
from app.db.sqlite.playlists import SQLitePlaylistMethods
|
||||
from app.db.store import Store
|
||||
from app.lib import playlistlib
|
||||
from app.utils import create_new_date, remove_duplicates
|
||||
|
||||
playlistbp = Blueprint("playlist", __name__, url_prefix="/")
|
||||
|
||||
PL = SQLitePlaylistMethods
|
||||
|
||||
insert_one_playlist = PL.insert_one_playlist
|
||||
get_playlist_by_name = PL.get_playlist_by_name
|
||||
count_playlist_by_name = PL.count_playlist_by_name
|
||||
get_all_playlists = PL.get_all_playlists
|
||||
get_playlist_by_id = PL.get_playlist_by_id
|
||||
tracks_to_playlist = PL.add_tracks_to_playlist
|
||||
add_artist_to_playlist = PL.add_artist_to_playlist
|
||||
update_playlist = PL.update_playlist
|
||||
delete_playlist = PL.delete_playlist
|
||||
|
||||
# get_tracks_by_trackhashes = SQLiteTrackMethods.get_tracks_by_trackhashes
|
||||
|
||||
|
||||
@playlistbp.route("/playlists", methods=["GET"])
|
||||
def send_all_playlists():
|
||||
"""
|
||||
Gets all the playlists.
|
||||
"""
|
||||
playlists = get_all_playlists()
|
||||
playlists = list(playlists)
|
||||
|
||||
playlists.sort(
|
||||
key=lambda p: datetime.strptime(p.last_updated, "%Y-%m-%d %H:%M:%S"),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
return {"data": playlists}
|
||||
|
||||
|
||||
@playlistbp.route("/playlist/new", methods=["POST"])
|
||||
def create_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 = count_playlist_by_name(data["name"])
|
||||
|
||||
if existing_playlist_count > 0:
|
||||
return {"error": "Playlist already exists"}, 409
|
||||
|
||||
playlist = {
|
||||
"artisthashes": json.dumps([]),
|
||||
"banner_pos": 50,
|
||||
"has_gif": 0,
|
||||
"image": None,
|
||||
"last_updated": create_new_date(),
|
||||
"name": data["name"],
|
||||
"trackhashes": json.dumps([]),
|
||||
}
|
||||
|
||||
playlist = insert_one_playlist(playlist)
|
||||
|
||||
if playlist is None:
|
||||
return {"error": "Playlist could not be created"}, 500
|
||||
|
||||
return {"playlist": playlist}, 201
|
||||
|
||||
|
||||
@playlistbp.route("/playlist/<playlist_id>/add", methods=["POST"])
|
||||
def add_track_to_playlist(playlist_id: str):
|
||||
"""
|
||||
Takes a playlist ID and a track hash, and adds the track to the playlist
|
||||
"""
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return {"error": "Track hash not provided"}, 400
|
||||
|
||||
trackhash = data["track"]
|
||||
|
||||
insert_count = tracks_to_playlist(int(playlist_id), [trackhash])
|
||||
|
||||
if insert_count == 0:
|
||||
return {"error": "Track already exists in playlist"}, 409
|
||||
|
||||
add_artist_to_playlist(int(playlist_id), trackhash)
|
||||
|
||||
return {"msg": "Done"}, 200
|
||||
|
||||
|
||||
@playlistbp.route("/playlist/<playlistid>")
|
||||
def get_playlist(playlistid: str):
|
||||
"""
|
||||
Gets a playlist by id, and if it exists, it gets all the tracks in the playlist and returns them.
|
||||
"""
|
||||
playlist = get_playlist_by_id(int(playlistid))
|
||||
|
||||
if playlist is None:
|
||||
return {"msg": "Playlist not found"}, 404
|
||||
|
||||
tracks = Store.get_tracks_by_trackhashes(list(playlist.trackhashes))
|
||||
tracks = remove_duplicates(tracks)
|
||||
|
||||
duration = sum(t.duration for t in tracks)
|
||||
playlist.last_updated = serializer.date_string_to_time_passed(playlist.last_updated)
|
||||
|
||||
playlist.duration = duration
|
||||
|
||||
return {"info": playlist, "tracks": tracks}
|
||||
|
||||
|
||||
@playlistbp.route("/playlist/<playlistid>/update", methods=["PUT"])
|
||||
def update_playlist_info(playlistid: str):
|
||||
if playlistid is None:
|
||||
return {"error": "Playlist ID not provided"}, 400
|
||||
|
||||
db_playlist = get_playlist_by_id(int(playlistid))
|
||||
|
||||
if db_playlist is None:
|
||||
return {"error": "Playlist not found"}, 404
|
||||
|
||||
image = None
|
||||
|
||||
if "image" in request.files:
|
||||
image = request.files["image"]
|
||||
|
||||
data = request.form
|
||||
|
||||
playlist = {
|
||||
"id": int(playlistid),
|
||||
"artisthashes": json.dumps([]),
|
||||
"banner_pos": db_playlist.banner_pos,
|
||||
"has_gif": 0,
|
||||
"image": db_playlist.image,
|
||||
"last_updated": create_new_date(),
|
||||
"name": str(data.get("name")).strip(),
|
||||
"trackhashes": json.dumps([]),
|
||||
}
|
||||
|
||||
if image:
|
||||
try:
|
||||
playlist["image"] = playlistlib.save_p_image(image, playlistid)
|
||||
|
||||
if image.content_type == "image/gif":
|
||||
playlist["has_gif"] = 1
|
||||
|
||||
# reset banner position to center.
|
||||
playlist["banner_pos"] = 50
|
||||
PL.update_banner_pos(int(playlistid), 50)
|
||||
|
||||
except UnidentifiedImageError:
|
||||
return {"error": "Failed: Invalid image"}, 400
|
||||
|
||||
p_tuple = (*playlist.values(),)
|
||||
print("banner pos:", playlist["banner_pos"])
|
||||
|
||||
update_playlist(int(playlistid), playlist)
|
||||
|
||||
playlist = models.Playlist(*p_tuple)
|
||||
playlist.last_updated = serializer.date_string_to_time_passed(playlist.last_updated)
|
||||
|
||||
return {
|
||||
"data": playlist,
|
||||
}
|
||||
|
||||
|
||||
# @playlist_bp.route("/playlist/artists", methods=["POST"])
|
||||
# def get_playlist_artists():
|
||||
# data = request.get_json()
|
||||
|
||||
# pid = data["pid"]
|
||||
# artists = playlistlib.GetPlaylistArtists(pid)()
|
||||
|
||||
# return {"data": artists}
|
||||
|
||||
|
||||
@playlistbp.route("/playlist/delete", methods=["POST"])
|
||||
def remove_playlist():
|
||||
"""
|
||||
Deletes a playlist by ID.
|
||||
"""
|
||||
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
|
||||
|
||||
delete_playlist(pid)
|
||||
|
||||
return {"msg": "Done"}, 200
|
||||
|
||||
|
||||
@playlistbp.route("/playlist/<pid>/set-image-pos", methods=["POST"])
|
||||
def update_image_position(pid: int):
|
||||
data = request.get_json()
|
||||
message = {"msg": "No data provided"}
|
||||
|
||||
if data is None:
|
||||
return message, 400
|
||||
|
||||
try:
|
||||
pos = data["pos"]
|
||||
except KeyError:
|
||||
return message, 400
|
||||
|
||||
PL.update_banner_pos(pid, pos)
|
||||
|
||||
return {"msg": "Image position saved"}, 200
|
||||
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
Contains all the search routes.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request
|
||||
|
||||
from app import models, utils
|
||||
from app.db.store import Store
|
||||
from app.lib import searchlib
|
||||
|
||||
searchbp = Blueprint("search", __name__, url_prefix="/")
|
||||
|
||||
|
||||
SEARCH_COUNT = 12
|
||||
"""The max amount of items to return per request"""
|
||||
|
||||
|
||||
class SearchResults:
|
||||
"""
|
||||
Holds all the search results.
|
||||
"""
|
||||
|
||||
query: str = ""
|
||||
tracks: list[models.Track] = []
|
||||
albums: list[models.Album] = []
|
||||
playlists: list[models.Playlist] = []
|
||||
artists: list[models.Artist] = []
|
||||
|
||||
|
||||
class DoSearch:
|
||||
"""Class containing the methods that perform searching."""
|
||||
|
||||
def __init__(self, query: str) -> None:
|
||||
"""
|
||||
:param :str:`query`: the search query.
|
||||
"""
|
||||
self.tracks: list[models.Track] = []
|
||||
self.query = query
|
||||
SearchResults.query = query
|
||||
|
||||
def search_tracks(self):
|
||||
"""Calls :class:`SearchTracks` which returns the tracks that fuzzily match
|
||||
the search terms. Then adds them to the `SearchResults` store.
|
||||
"""
|
||||
self.tracks = Store.tracks
|
||||
tracks = searchlib.SearchTracks(self.tracks, self.query)()
|
||||
|
||||
if len(tracks) == 0:
|
||||
return []
|
||||
|
||||
tracks = utils.remove_duplicates(tracks)
|
||||
SearchResults.tracks = tracks
|
||||
|
||||
return tracks
|
||||
|
||||
def search_artists(self):
|
||||
"""Calls :class:`SearchArtists` which returns the artists that fuzzily match
|
||||
the search term. Then adds them to the `SearchResults` store.
|
||||
"""
|
||||
# self.artists = utils.Get.get_all_artists()
|
||||
artists = [a.name for a in Store.artists]
|
||||
artists = searchlib.SearchArtists(artists, self.query)()
|
||||
SearchResults.artists = artists
|
||||
|
||||
return artists
|
||||
|
||||
def search_albums(self):
|
||||
"""Calls :class:`SearchAlbums` which returns the albums that fuzzily match
|
||||
the search term. Then adds them to the `SearchResults` store.
|
||||
"""
|
||||
# albums = utils.Get.get_all_albums()
|
||||
albums = Store.albums
|
||||
albums = searchlib.SearchAlbums(albums, self.query)()
|
||||
SearchResults.albums = albums
|
||||
|
||||
return albums
|
||||
|
||||
# def search_playlists(self):
|
||||
# """Calls :class:`SearchPlaylists` which returns the playlists that fuzzily match
|
||||
# the search term. Then adds them to the `SearchResults` store.
|
||||
# """
|
||||
# playlists = utils.Get.get_all_playlists()
|
||||
# playlists = [serializer.Playlist(playlist) for playlist in playlists]
|
||||
|
||||
# playlists = searchlib.SearchPlaylists(playlists, self.query)()
|
||||
# SearchResults.playlists = playlists
|
||||
|
||||
# return playlists
|
||||
|
||||
def search_all(self):
|
||||
"""Calls all the search methods."""
|
||||
self.search_tracks()
|
||||
self.search_albums()
|
||||
self.search_artists()
|
||||
# self.search_playlists()
|
||||
|
||||
|
||||
@searchbp.route("/search/tracks", methods=["GET"])
|
||||
def search_tracks():
|
||||
"""
|
||||
Searches for tracks that match the search query.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
tracks = DoSearch(query).search_tracks()
|
||||
|
||||
return {
|
||||
"tracks": tracks[:SEARCH_COUNT],
|
||||
"more": len(tracks) > SEARCH_COUNT,
|
||||
}
|
||||
|
||||
|
||||
@searchbp.route("/search/albums", methods=["GET"])
|
||||
def search_albums():
|
||||
"""
|
||||
Searches for albums.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
tracks = DoSearch(query).search_albums()
|
||||
|
||||
return {
|
||||
"albums": tracks[:SEARCH_COUNT],
|
||||
"more": len(tracks) > SEARCH_COUNT,
|
||||
}
|
||||
|
||||
|
||||
@searchbp.route("/search/artists", methods=["GET"])
|
||||
def search_artists():
|
||||
"""
|
||||
Searches for artists.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
artists = DoSearch(query).search_artists()
|
||||
|
||||
return {
|
||||
"artists": artists[:SEARCH_COUNT],
|
||||
"more": len(artists) > SEARCH_COUNT,
|
||||
}
|
||||
|
||||
|
||||
# @searchbp.route("/search/playlists", methods=["GET"])
|
||||
# def search_playlists():
|
||||
# """
|
||||
# Searches for playlists.
|
||||
# """
|
||||
|
||||
# query = request.args.get("q")
|
||||
# if not query:
|
||||
# return {"error": "No query provided"}, 400
|
||||
|
||||
# playlists = DoSearch(query).search_playlists()
|
||||
|
||||
# return {
|
||||
# "playlists": playlists[:SEARCH_COUNT],
|
||||
# "more": len(playlists) > SEARCH_COUNT,
|
||||
# }
|
||||
|
||||
|
||||
@searchbp.route("/search/top", methods=["GET"])
|
||||
def get_top_results():
|
||||
"""
|
||||
Returns the top results for the search query.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
DoSearch(query).search_all()
|
||||
|
||||
max = 2
|
||||
return {
|
||||
"tracks": SearchResults.tracks[:max],
|
||||
"albums": SearchResults.albums[:max],
|
||||
"artists": SearchResults.artists[:max],
|
||||
"playlists": SearchResults.playlists[:max],
|
||||
}
|
||||
|
||||
|
||||
@searchbp.route("/search/loadmore")
|
||||
def search_load_more():
|
||||
"""
|
||||
Returns more songs, albums or artists from a search query.
|
||||
"""
|
||||
s_type = request.args.get("type")
|
||||
index = int(request.args.get("index") or 0)
|
||||
|
||||
if s_type == "tracks":
|
||||
t = SearchResults.tracks
|
||||
return {
|
||||
"tracks": t[index : index + SEARCH_COUNT],
|
||||
"more": len(t) > index + SEARCH_COUNT,
|
||||
}
|
||||
|
||||
elif s_type == "albums":
|
||||
a = SearchResults.albums
|
||||
return {
|
||||
"albums": a[index : index + SEARCH_COUNT],
|
||||
"more": len(a) > index + SEARCH_COUNT,
|
||||
}
|
||||
|
||||
elif s_type == "artists":
|
||||
a = SearchResults.artists
|
||||
return {
|
||||
"artists": a[index : index + SEARCH_COUNT],
|
||||
"more": len(a) > index + SEARCH_COUNT,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Contains all the track routes.
|
||||
"""
|
||||
from flask import Blueprint, send_file
|
||||
from app.db.store import Store
|
||||
|
||||
trackbp = Blueprint("track", __name__, url_prefix="/")
|
||||
|
||||
|
||||
@trackbp.route("/file/<trackhash>")
|
||||
def send_track_file(trackhash: str):
|
||||
"""
|
||||
Returns an audio file that matches the passed id to the client.
|
||||
Falls back to track hash if id is not found.
|
||||
"""
|
||||
msg = {"msg": "File Not Found"}
|
||||
if trackhash is None:
|
||||
return msg, 404
|
||||
|
||||
try:
|
||||
track = Store.get_tracks_by_trackhashes([trackhash])[0]
|
||||
except IndexError:
|
||||
track = None
|
||||
|
||||
if track is None:
|
||||
return msg, 404
|
||||
|
||||
audio_type = track.filepath.rsplit(".", maxsplit=1)[-1]
|
||||
|
||||
try:
|
||||
return send_file(track.filepath, mimetype=f"audio/{audio_type}")
|
||||
except FileNotFoundError:
|
||||
return msg, 404
|
||||
Reference in New Issue
Block a user