Merge pull request #70 from geoffrey45/remove-in-memory-implementations
- Remove global lists and read everything from the database. ✅ - Restyle album page ✅ ✨ - Fix watchdog ✅ - Fix album counter ✅ - Show playlist duration ✅ - and much more.
@@ -18,6 +18,7 @@
|
||||
"register-service-worker": "^1.7.1",
|
||||
"sass": "^1.49.0",
|
||||
"sass-loader": "^10",
|
||||
"vite-svg-loader": "^3.4.0",
|
||||
"vue": "^3.0.0",
|
||||
"vue-debounce": "^3.0.2",
|
||||
"vue-router": "^4.0.0-0",
|
||||
@@ -28,7 +29,8 @@
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-plugin-vue": "^8.3.0",
|
||||
"vite": "^2.5.4"
|
||||
"vite": "^2.5.4",
|
||||
"vue-svg-loader": "^0.16.0"
|
||||
},
|
||||
"packageManager": "yarn@3.1.1"
|
||||
}
|
||||
|
||||
@@ -3,25 +3,9 @@ This module contains all the Flask Blueprints and API routes. It also contains a
|
||||
that are used through-out the app. It handles the initialization of the watchdog,
|
||||
checking and creating config dirs and starting the re-indexing process using a background thread.
|
||||
"""
|
||||
from typing import List
|
||||
from typing import Set
|
||||
|
||||
from app import functions
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from app import models
|
||||
from app import prep
|
||||
from app.lib import albumslib
|
||||
from app.lib import folderslib
|
||||
from app.lib import playlistlib
|
||||
|
||||
DB_TRACKS = instances.tracks_instance.get_all_tracks()
|
||||
VALID_FOLDERS: Set[str] = set()
|
||||
|
||||
ALBUMS: List[models.Album] = []
|
||||
TRACKS: List[models.Track] = []
|
||||
PLAYLISTS: List[models.Playlist] = []
|
||||
FOLDERS: List[models.Folder] = List
|
||||
|
||||
|
||||
@helpers.background
|
||||
@@ -31,10 +15,7 @@ def initialize() -> None:
|
||||
"""
|
||||
functions.start_watchdog()
|
||||
prep.create_config_dir()
|
||||
albumslib.create_everything()
|
||||
folderslib.run_scandir()
|
||||
playlistlib.create_all_playlists()
|
||||
functions.reindex_tracks()
|
||||
functions.run_checks()
|
||||
|
||||
|
||||
initialize()
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
"""
|
||||
Contains all the album routes.
|
||||
"""
|
||||
from pprint import pprint
|
||||
from typing import List
|
||||
|
||||
from app import api
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.functions import FetchAlbumBio
|
||||
from app.lib import albumslib
|
||||
from app.lib import trackslib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
from app.functions import FetchAlbumBio
|
||||
|
||||
album_bp = Blueprint("album", __name__, url_prefix="")
|
||||
|
||||
@@ -35,42 +36,57 @@ def get_albums():
|
||||
return {"albums": albums}
|
||||
|
||||
|
||||
@album_bp.route("/album/tracks", methods=["POST"])
|
||||
@album_bp.route("/album", methods=["POST"])
|
||||
def get_album():
|
||||
"""Returns all the tracks in the given album."""
|
||||
data = request.get_json()
|
||||
albumhash = data["hash"]
|
||||
error_msg = {"error": "Album not created yet."}
|
||||
|
||||
album = data["album"]
|
||||
artist = data["artist"]
|
||||
tracks = instances.tracks_instance.find_tracks_by_hash(albumhash)
|
||||
|
||||
songs = trackslib.get_album_tracks(album, artist)
|
||||
albumhash = helpers.create_album_hash(album, artist)
|
||||
index = albumslib.find_album(api.ALBUMS, albumhash)
|
||||
album: models.Album = api.ALBUMS[index]
|
||||
if len(tracks) == 0:
|
||||
return error_msg, 204
|
||||
|
||||
album.count = len(songs)
|
||||
album.duration = albumslib.get_album_duration(songs)
|
||||
tracks = [models.Track(t) for t in tracks]
|
||||
tracks = helpers.RemoveDuplicates(tracks)()
|
||||
|
||||
if (
|
||||
album.count == 1
|
||||
and songs[0].title == album.title
|
||||
and songs[0].tracknumber == 1
|
||||
and songs[0].disknumber == 1
|
||||
):
|
||||
album = instances.album_instance.find_album_by_hash(albumhash)
|
||||
|
||||
if not album:
|
||||
return error_msg, 204
|
||||
|
||||
album = models.Album(album)
|
||||
|
||||
album.count = len(tracks)
|
||||
try:
|
||||
album.duration = sum([t.length for t in tracks])
|
||||
except AttributeError:
|
||||
album.duration = 0
|
||||
|
||||
if (album.count == 1 and tracks[0].title == album.title
|
||||
and tracks[0].tracknumber == 1 and tracks[0].disknumber == 1):
|
||||
album.is_single = True
|
||||
|
||||
return {"songs": songs, "info": album}
|
||||
return {"tracks": tracks, "info": album}
|
||||
|
||||
|
||||
@album_bp.route("/album/bio", methods=["POST"])
|
||||
def get_album_bio():
|
||||
"""Returns the album bio for the given album."""
|
||||
data = request.get_json()
|
||||
fetch_bio = FetchAlbumBio(data["album"], data["albumartist"])
|
||||
bio = fetch_bio()
|
||||
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 {"bio": "No bio found."}, 404
|
||||
return err_msg, 404
|
||||
|
||||
return {"bio": bio}
|
||||
|
||||
@@ -80,19 +96,16 @@ def get_albumartists():
|
||||
"""Returns a list of artists featured in a given album."""
|
||||
data = request.get_json()
|
||||
|
||||
album = data["album"]
|
||||
artist = data["artist"]
|
||||
albumhash = data["hash"]
|
||||
|
||||
tracks: List[models.Track] = []
|
||||
|
||||
for track in api.TRACKS:
|
||||
if track.album == album and track.albumartist == artist:
|
||||
tracks.append(track)
|
||||
tracks = instances.tracks_instance.find_tracks_by_hash(albumhash)
|
||||
tracks = [models.Track(t) for t in tracks]
|
||||
|
||||
artists = []
|
||||
|
||||
for track in tracks:
|
||||
for artist in track.artists:
|
||||
artist = artist.lower()
|
||||
if artist not in artists:
|
||||
artists.append(artist)
|
||||
|
||||
@@ -100,7 +113,7 @@ def get_albumartists():
|
||||
for artist in artists:
|
||||
artist_obj = {
|
||||
"name": artist,
|
||||
"image": helpers.check_artist_image(artist),
|
||||
"image": helpers.create_safe_name(artist) + ".webp",
|
||||
}
|
||||
final_artists.append(artist_obj)
|
||||
|
||||
|
||||
@@ -10,49 +10,48 @@ from flask import Blueprint
|
||||
|
||||
artist_bp = Blueprint("artist", __name__, url_prefix="/")
|
||||
|
||||
# @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)
|
||||
|
||||
@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)
|
||||
|
||||
def get_artist_tracks():
|
||||
songs = instances.tracks_instance.find_songs_by_artist(artist)
|
||||
# return songs
|
||||
|
||||
return songs
|
||||
# artist_songs = get_artist_tracks()
|
||||
# songs = helpers.remove_duplicates(artist_songs)
|
||||
|
||||
artist_songs = get_artist_tracks()
|
||||
songs = helpers.remove_duplicates(artist_songs)
|
||||
# def get_artist_albums():
|
||||
# artist_albums = []
|
||||
# albums_with_count = []
|
||||
|
||||
def get_artist_albums():
|
||||
artist_albums = []
|
||||
albums_with_count = []
|
||||
# albums = instances.tracks_instance.find_songs_by_albumartist(artist)
|
||||
|
||||
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 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 album in artist_albums:
|
||||
count = 0
|
||||
length = 0
|
||||
# for song in artist_songs:
|
||||
# if song["album"] == album:
|
||||
# count = count + 1
|
||||
# length = length + song["length"]
|
||||
|
||||
for song in artist_songs:
|
||||
if song["album"] == album:
|
||||
count = count + 1
|
||||
length = length + song["length"]
|
||||
# album_ = {"title": album, "count": count, "length": length}
|
||||
|
||||
album_ = {"title": album, "count": count, "length": length}
|
||||
# albums_with_count.append(album_)
|
||||
|
||||
albums_with_count.append(album_)
|
||||
# return albums_with_count
|
||||
|
||||
return albums_with_count
|
||||
|
||||
return {
|
||||
"artist": artist_obj,
|
||||
"songs": songs,
|
||||
"albums": get_artist_albums()
|
||||
}
|
||||
# return {
|
||||
# "artist": artist_obj,
|
||||
# "songs": songs,
|
||||
# "albums": get_artist_albums()
|
||||
# }
|
||||
|
||||
@@ -8,12 +8,13 @@ from app import exceptions
|
||||
from app import instances
|
||||
from app import models
|
||||
from app import serializer
|
||||
from app.helpers import create_new_date
|
||||
from app.helpers import Get
|
||||
from app.helpers import UseBisection
|
||||
from app.lib import playlistlib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
from app.helpers import UseBisection, create_new_date
|
||||
|
||||
playlist_bp = Blueprint("playlist", __name__, url_prefix="/")
|
||||
|
||||
PlaylistExists = exceptions.PlaylistExists
|
||||
@@ -22,8 +23,13 @@ TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
|
||||
|
||||
@playlist_bp.route("/playlists", methods=["GET"])
|
||||
def get_all_playlists():
|
||||
"""Returns all the playlists."""
|
||||
dbplaylists = instances.playlist_instance.get_all_playlists()
|
||||
dbplaylists = [models.Playlist(p) for p in dbplaylists]
|
||||
|
||||
playlists = [
|
||||
serializer.Playlist(p, construct_last_updated=False) for p in api.PLAYLISTS
|
||||
serializer.Playlist(p, construct_last_updated=False)
|
||||
for p in dbplaylists
|
||||
]
|
||||
playlists.sort(
|
||||
key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"),
|
||||
@@ -36,7 +42,7 @@ def get_all_playlists():
|
||||
def create_playlist():
|
||||
data = request.get_json()
|
||||
|
||||
playlist = {
|
||||
data = {
|
||||
"name": data["name"],
|
||||
"description": "",
|
||||
"pre_tracks": [],
|
||||
@@ -45,21 +51,16 @@ def create_playlist():
|
||||
"thumb": "",
|
||||
}
|
||||
|
||||
try:
|
||||
for pl in api.PLAYLISTS:
|
||||
if pl.name == playlist["name"]:
|
||||
raise PlaylistExists("Playlist already exists.")
|
||||
dbp = instances.playlist_instance.get_playlist_by_name(data["name"])
|
||||
|
||||
except PlaylistExists as e:
|
||||
return {"error": str(e)}, 409
|
||||
if dbp is not None:
|
||||
return {"message": "Playlist already exists."}, 409
|
||||
|
||||
upsert_id = instances.playlist_instance.insert_playlist(playlist)
|
||||
upsert_id = instances.playlist_instance.insert_playlist(data)
|
||||
p = instances.playlist_instance.get_playlist_by_id(upsert_id)
|
||||
pp = models.Playlist(p)
|
||||
playlist = models.Playlist(p)
|
||||
|
||||
api.PLAYLISTS.append(pp)
|
||||
|
||||
return {"playlist": pp}, 201
|
||||
return {"playlist": playlist}, 201
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/<playlist_id>/add", methods=["POST"])
|
||||
@@ -70,22 +71,27 @@ def add_track_to_playlist(playlist_id: str):
|
||||
|
||||
try:
|
||||
playlistlib.add_track(playlist_id, trackid)
|
||||
except TrackExistsInPlaylist as e:
|
||||
except TrackExistsInPlaylist:
|
||||
return {"error": "Track already exists in playlist"}, 409
|
||||
|
||||
return {"msg": "I think It's done"}, 200
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/<playlistid>")
|
||||
def get_single_p_info(playlistid: str):
|
||||
p = UseBisection(api.PLAYLISTS, "playlistid", [playlistid])()
|
||||
playlist: models.Playlist = p[0]
|
||||
def get_playlist(playlistid: str):
|
||||
p = instances.playlist_instance.get_playlist_by_id(playlistid)
|
||||
if p is None:
|
||||
return {"info": {}, "tracks": []}
|
||||
|
||||
if playlist is not None:
|
||||
tracks = playlist.get_tracks()
|
||||
return {"info": serializer.Playlist(playlist), "tracks": tracks}
|
||||
playlist = models.Playlist(p)
|
||||
|
||||
return {"info": {}, "tracks": []}
|
||||
tracks = playlistlib.create_playlist_tracks(playlist.pretracks)
|
||||
|
||||
duration = sum([t.length for t in tracks])
|
||||
playlist = serializer.Playlist(playlist)
|
||||
playlist.duration = duration
|
||||
|
||||
return {"info": playlist, "tracks": tracks}
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/<playlistid>/update", methods=["PUT"])
|
||||
@@ -105,25 +111,27 @@ def update_playlist(playlistid: str):
|
||||
"thumb": None,
|
||||
}
|
||||
|
||||
p = UseBisection(api.PLAYLISTS, "playlistid", [playlistid])()
|
||||
playlists = Get.get_all_playlists()
|
||||
|
||||
p = UseBisection(playlists, "playlistid", [playlistid])()
|
||||
p: models.Playlist = p[0]
|
||||
|
||||
if playlist is not None:
|
||||
if image:
|
||||
image_, thumb_ = playlistlib.save_p_image(image, playlistid)
|
||||
playlist["image"] = image_
|
||||
playlist["thumb"] = thumb_
|
||||
if image:
|
||||
image_, thumb_ = playlistlib.save_p_image(image, playlistid)
|
||||
playlist["image"] = image_
|
||||
playlist["thumb"] = thumb_
|
||||
|
||||
else:
|
||||
playlist["image"] = p.image.split("/")[-1]
|
||||
playlist["thumb"] = p.thumb.split("/")[-1]
|
||||
else:
|
||||
playlist["image"] = p.image.split("/")[-1]
|
||||
playlist["thumb"] = p.thumb.split("/")[-1]
|
||||
|
||||
p.update_playlist(playlist)
|
||||
instances.playlist_instance.update_playlist(playlistid, playlist)
|
||||
p.update_playlist(playlist)
|
||||
instances.playlist_instance.update_playlist(playlistid, playlist)
|
||||
|
||||
return {
|
||||
"data": serializer.Playlist(p),
|
||||
}
|
||||
return {
|
||||
"data": serializer.Playlist(p),
|
||||
}
|
||||
|
||||
return {"msg": "Something shady happened"}, 500
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""
|
||||
Contains all the search routes.
|
||||
"""
|
||||
from pprint import pprint
|
||||
from typing import List
|
||||
|
||||
from app import helpers
|
||||
from app import models
|
||||
from app import serializer
|
||||
from app.lib import searchlib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
@@ -15,22 +20,94 @@ SEARCH_RESULTS = {
|
||||
}
|
||||
|
||||
|
||||
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.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 = helpers.Get.get_all_tracks()
|
||||
tracks = searchlib.SearchTracks(self.tracks, self.query)()
|
||||
tracks = helpers.RemoveDuplicates(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 = helpers.Get.get_all_artists()
|
||||
artists = searchlib.SearchArtists(self.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 = helpers.Get.get_all_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 = helpers.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()
|
||||
|
||||
|
||||
@search_bp.route("/search/tracks", methods=["GET"])
|
||||
def search_tracks():
|
||||
"""
|
||||
Searches for tracks.
|
||||
Searches for tracks that match the search query.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
results = searchlib.SearchTracks(query)()
|
||||
SEARCH_RESULTS["tracks"] = results
|
||||
tracks = DoSearch(query).search_tracks()
|
||||
|
||||
return {
|
||||
"tracks": results[:5],
|
||||
"more": len(results) > 5,
|
||||
"tracks": tracks[:6],
|
||||
"more": len(tracks) > 6,
|
||||
}, 200
|
||||
|
||||
|
||||
@@ -44,12 +121,11 @@ def search_albums():
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
results = searchlib.SearchAlbums(query)()
|
||||
SEARCH_RESULTS["albums"] = results
|
||||
tracks = DoSearch(query).search_albums()
|
||||
|
||||
return {
|
||||
"albums": results[:6],
|
||||
"more": len(results) > 6,
|
||||
"albums": tracks[:6],
|
||||
"more": len(tracks) > 6,
|
||||
}, 200
|
||||
|
||||
|
||||
@@ -63,51 +139,50 @@ def search_artists():
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
results = searchlib.SearchArtists(query)()
|
||||
SEARCH_RESULTS["artists"] = results
|
||||
artists = DoSearch(query).search_artists()
|
||||
|
||||
return {
|
||||
"artists": results[:6],
|
||||
"more": len(results) > 6,
|
||||
"artists": artists[:6],
|
||||
"more": len(artists) > 6,
|
||||
}, 200
|
||||
|
||||
|
||||
@search_bp.route("/search")
|
||||
def search():
|
||||
@search_bp.route("/search/playlists", methods=["GET"])
|
||||
def search_playlists():
|
||||
"""
|
||||
Returns a list of songs, albums and artists that match the search query.
|
||||
Searches for playlists.
|
||||
"""
|
||||
query = request.args.get("q") or "Mexican girl"
|
||||
|
||||
albums = searchlib.SearchAlbums(query)()
|
||||
artists_dicts = searchlib.SearchArtists(query)()
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
tracks = searchlib.SearchTracks(query)()
|
||||
top_artist = artists_dicts[0]["name"]
|
||||
|
||||
_tracks = searchlib.GetTopArtistTracks(top_artist)()
|
||||
tracks = [*tracks, *[t for t in _tracks if t not in tracks]]
|
||||
|
||||
SEARCH_RESULTS.clear()
|
||||
SEARCH_RESULTS["tracks"] = tracks
|
||||
SEARCH_RESULTS["albums"] = albums
|
||||
SEARCH_RESULTS["artists"] = artists_dicts
|
||||
playlists = DoSearch(query).search_playlists()
|
||||
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"tracks": tracks[:5],
|
||||
"more": len(tracks) > 5
|
||||
},
|
||||
{
|
||||
"albums": albums[:6],
|
||||
"more": len(albums) > 6
|
||||
},
|
||||
{
|
||||
"artists": artists_dicts[:6],
|
||||
"more": len(artists_dicts) > 6
|
||||
},
|
||||
]
|
||||
"playlists": playlists[:6],
|
||||
"more": len(playlists) > 6,
|
||||
}, 200
|
||||
|
||||
|
||||
@search_bp.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],
|
||||
}
|
||||
|
||||
|
||||
@@ -120,19 +195,22 @@ def search_load_more():
|
||||
index = int(request.args.get("index"))
|
||||
|
||||
if type == "tracks":
|
||||
t = SearchResults.tracks
|
||||
return {
|
||||
"tracks": SEARCH_RESULTS["tracks"][index:index + 5],
|
||||
"more": len(SEARCH_RESULTS["tracks"]) > index + 5,
|
||||
"tracks": t[index:index + 5],
|
||||
"more": len(t) > index + 5,
|
||||
}
|
||||
|
||||
elif type == "albums":
|
||||
a = SearchResults.albums
|
||||
return {
|
||||
"albums": SEARCH_RESULTS["albums"][index:index + 6],
|
||||
"more": len(SEARCH_RESULTS["albums"]) > index + 6,
|
||||
"albums": a[index:index + 6],
|
||||
"more": len(a) > index + 6,
|
||||
}
|
||||
|
||||
elif type == "artists":
|
||||
a = SearchResults.artists
|
||||
return {
|
||||
"artists": SEARCH_RESULTS["artists"][index:index + 6],
|
||||
"more": len(SEARCH_RESULTS["artists"]) > index + 6,
|
||||
"artists": a[index:index + 6],
|
||||
"more": len(a) > index + 6,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ Contains all the track routes.
|
||||
"""
|
||||
from app import api
|
||||
from app import instances
|
||||
from app import models
|
||||
from flask import Blueprint
|
||||
from flask import send_file
|
||||
|
||||
@@ -14,21 +15,19 @@ def send_track_file(trackid):
|
||||
"""
|
||||
Returns an audio file that matches the passed id to the client.
|
||||
"""
|
||||
track = instances.tracks_instance.get_track_by_id(trackid)
|
||||
msg = {"msg": "File Not Found"}
|
||||
|
||||
if track is None:
|
||||
return msg, 404
|
||||
|
||||
track = models.Track(track)
|
||||
type = track.filepath.split(".")[-1]
|
||||
|
||||
try:
|
||||
files = []
|
||||
for f in api.DB_TRACKS:
|
||||
try:
|
||||
if f["_id"]["$oid"] == trackid:
|
||||
files.append(f["filepath"])
|
||||
except KeyError:
|
||||
# Bug: some albums are not found although they exist in `api.ALBUMS`. It has something to do with the bisection method used or sorting. Not sure yet.
|
||||
pass
|
||||
|
||||
filepath = files[0]
|
||||
except IndexError:
|
||||
return "File not found", 404
|
||||
|
||||
return send_file(filepath, mimetype="audio/mp3")
|
||||
return send_file(track.filepath, mimetype=f"audio/{type}")
|
||||
except FileNotFoundError:
|
||||
return msg, 404
|
||||
|
||||
|
||||
@track_bp.route("/sample")
|
||||
@@ -37,6 +36,5 @@ def get_sample_track():
|
||||
Returns a sample track object.
|
||||
"""
|
||||
|
||||
return instances.tracks_instance.get_song_by_album(
|
||||
"Legends Never Die", "Juice WRLD"
|
||||
)
|
||||
return instances.tracks_instance.get_song_by_album("Legends Never Die",
|
||||
"Juice WRLD")
|
||||
|
||||
@@ -200,3 +200,15 @@ class TrackMethods:
|
||||
Removes a track from the database. Returns a boolean indicating success or failure of the operation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_hash():
|
||||
"""
|
||||
Returns all the tracks matching the passed hash.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_dir_t_count():
|
||||
"""
|
||||
Returns a list of all the tracks matching the path in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
This file contains the Album class for interacting with
|
||||
album documents in MongoDB.
|
||||
"""
|
||||
from app import db
|
||||
from typing import List
|
||||
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
from app.db.mongodb import MongoAlbums
|
||||
@@ -31,6 +32,13 @@ class Albums(MongoAlbums):
|
||||
upsert=True,
|
||||
).upserted_id
|
||||
|
||||
def insert_many(self, albums: Album):
|
||||
albums = [a.__dict__ for a in albums]
|
||||
"""
|
||||
Inserts multiple albums into the database.
|
||||
"""
|
||||
return self.collection.insert_many(albums)
|
||||
|
||||
def get_all_albums(self) -> list:
|
||||
"""
|
||||
Returns all the albums in the database.
|
||||
@@ -52,9 +60,20 @@ class Albums(MongoAlbums):
|
||||
album = self.collection.find_one({"album": name, "artist": artist})
|
||||
return convert_one(album)
|
||||
|
||||
def get_album_by_artist(self, name: str) -> dict:
|
||||
def find_album_by_hash(self, hash: str) -> dict:
|
||||
"""
|
||||
Returns a single album matching the artist in the query params.
|
||||
Returns a single album matching the hash in the query params.
|
||||
"""
|
||||
album = self.collection.find_one({"albumartist": name})
|
||||
album = self.collection.find_one({"hash": hash})
|
||||
return convert_one(album)
|
||||
|
||||
def set_album_colors(self, colors: List[str], hash: str) -> None:
|
||||
"""
|
||||
Sets the colors for an album.
|
||||
"""
|
||||
self.collection.update_one(
|
||||
{"hash": hash},
|
||||
{"$set": {
|
||||
"colors": colors
|
||||
}},
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
This file contains the Playlists class for interacting with the playlist documents in MongoDB.
|
||||
"""
|
||||
from app import helpers
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
from app.db.mongodb import MongoPlaylists
|
||||
from app.helpers import create_new_date
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
@@ -41,25 +41,32 @@ class Playlists(MongoPlaylists):
|
||||
playlist = self.collection.find_one({"_id": ObjectId(id)})
|
||||
return convert_one(playlist)
|
||||
|
||||
def set_last_updated(self, playlistid: str) -> None:
|
||||
"""
|
||||
Sets the lastUpdated field to the current date.
|
||||
"""
|
||||
date = helpers.create_new_date()
|
||||
|
||||
return self.collection.update_one(
|
||||
{"_id": ObjectId(playlistid)},
|
||||
{"$set": {
|
||||
"lastUpdated": date
|
||||
}},
|
||||
)
|
||||
|
||||
def add_track_to_playlist(self, playlistid: str, track: dict) -> None:
|
||||
"""
|
||||
Adds a track to a playlist.
|
||||
"""
|
||||
date = create_new_date()
|
||||
|
||||
return self.collection.update_one(
|
||||
self.collection.update_one(
|
||||
{
|
||||
"_id": ObjectId(playlistid),
|
||||
},
|
||||
{
|
||||
"$push": {
|
||||
"pre_tracks": track
|
||||
},
|
||||
"$set": {
|
||||
"lastUpdated": date
|
||||
}
|
||||
},
|
||||
{"$push": {
|
||||
"pre_tracks": track
|
||||
}},
|
||||
)
|
||||
self.set_last_updated(playlistid)
|
||||
|
||||
def get_playlist_by_name(self, name: str) -> dict:
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
This file contains the AllSongs class for interacting with track documents in MongoDB.
|
||||
"""
|
||||
import re
|
||||
|
||||
import pymongo
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
@@ -20,9 +22,18 @@ class Tracks(MongoTracks):
|
||||
"""
|
||||
Inserts a new track object into the database.
|
||||
"""
|
||||
return self.collection.update_one(
|
||||
{"filepath": song_obj["filepath"]}, {"$set": song_obj}, upsert=True
|
||||
).upserted_id
|
||||
return self.collection.update_one({
|
||||
"filepath": song_obj["filepath"]
|
||||
}, {
|
||||
"$set": song_obj
|
||||
},
|
||||
upsert=True).upserted_id
|
||||
|
||||
def insert_many(self, songs: list):
|
||||
"""
|
||||
Inserts multiple songs into the database.
|
||||
"""
|
||||
return self.collection.insert_many(songs)
|
||||
|
||||
def get_all_tracks(self) -> list:
|
||||
"""
|
||||
@@ -30,11 +41,11 @@ class Tracks(MongoTracks):
|
||||
"""
|
||||
return convert_many(self.collection.find())
|
||||
|
||||
def get_song_by_id(self, file_id: str) -> dict:
|
||||
def get_track_by_id(self, id: str) -> dict:
|
||||
"""
|
||||
Returns a track object by its mongodb id.
|
||||
"""
|
||||
song = self.collection.find_one({"_id": ObjectId(file_id)})
|
||||
song = self.collection.find_one({"_id": ObjectId(id)})
|
||||
return convert_one(song)
|
||||
|
||||
def get_song_by_album(self, name: str, artist: str) -> dict:
|
||||
@@ -48,21 +59,33 @@ class Tracks(MongoTracks):
|
||||
"""
|
||||
Returns all the songs matching the albums in the query params (using regex).
|
||||
"""
|
||||
songs = self.collection.find({"album": {"$regex": query, "$options": "i"}})
|
||||
songs = self.collection.find(
|
||||
{"album": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def search_songs_by_artist(self, query: str) -> list:
|
||||
"""
|
||||
Returns all the songs matching the artists in the query params.
|
||||
"""
|
||||
songs = self.collection.find({"artists": {"$regex": query, "$options": "i"}})
|
||||
songs = self.collection.find(
|
||||
{"artists": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_song_by_title(self, query: str) -> list:
|
||||
"""
|
||||
Finds all the tracks matching the title in the query params.
|
||||
"""
|
||||
song = self.collection.find({"title": {"$regex": query, "$options": "i"}})
|
||||
song = self.collection.find(
|
||||
{"title": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(song)
|
||||
|
||||
def find_songs_by_album(self, name: str, artist: str) -> list:
|
||||
@@ -76,16 +99,33 @@ class Tracks(MongoTracks):
|
||||
"""
|
||||
Returns a sorted list of all the tracks exactly matching the folder in the query params
|
||||
"""
|
||||
songs = self.collection.find({"folder": query}).sort("title", pymongo.ASCENDING)
|
||||
songs = self.collection.find({
|
||||
"folder": query
|
||||
}).sort("title", pymongo.ASCENDING)
|
||||
return convert_many(songs)
|
||||
|
||||
def find_songs_by_filenames(self, filenames: list) -> list:
|
||||
"""
|
||||
Returns a list of all the tracks matching the filenames in the query params.
|
||||
"""
|
||||
songs = self.collection.find({"filepath": {"$in": filenames}})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_songs_by_folder_og(self, query: str) -> list:
|
||||
"""
|
||||
Returns an unsorted list of all the tracks exactly matching the folder in the query params
|
||||
Returns an unsorted list of all the track matching the folder in the query params
|
||||
"""
|
||||
songs = self.collection.find({"folder": query})
|
||||
return convert_many(songs)
|
||||
|
||||
def get_dir_t_count(self, path: str) -> int:
|
||||
"""
|
||||
Returns a list of all the tracks matching the path in the query params.
|
||||
"""
|
||||
regex = re.compile(r"^.*" + re.escape(path) + r".*$")
|
||||
|
||||
return self.collection.count_documents({"filepath": {"$regex": regex}})
|
||||
|
||||
def find_songs_by_artist(self, query: str) -> list:
|
||||
"""
|
||||
Returns a list of all the tracks exactly matching the artists in the query params.
|
||||
@@ -98,8 +138,10 @@ class Tracks(MongoTracks):
|
||||
Returns a list of all the tracks containing the albumartist in the query params.
|
||||
"""
|
||||
songs = self.collection.find(
|
||||
{"albumartist": {"$regex": query, "$options": "i"}}
|
||||
)
|
||||
{"albumartist": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def get_song_by_path(self, path: str) -> dict:
|
||||
@@ -128,3 +170,22 @@ class Tracks(MongoTracks):
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def find_tracks_by_hash(self, hash: str) -> list:
|
||||
"""
|
||||
Returns a list of all the tracks matching the hash in the query params.
|
||||
"""
|
||||
songs = self.collection.find({"albumhash": hash})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_track_by_title_artists_album(self, title: str, artist: str,
|
||||
album: str) -> dict:
|
||||
"""
|
||||
Returns a single track matching the title, artist, and album in the query params.
|
||||
"""
|
||||
song = self.collection.find_one({
|
||||
"title": title,
|
||||
"artists": artist,
|
||||
"album": album
|
||||
})
|
||||
return convert_one(song)
|
||||
|
||||
@@ -3,31 +3,49 @@ This module contains functions for the server
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
from app import api
|
||||
from app import helpers
|
||||
from app import settings
|
||||
from app.lib import trackslib
|
||||
from app.lib import watchdoge
|
||||
from app.lib.albumslib import ValidateAlbumThumbs
|
||||
from app.lib.colorlib import ProcessAlbumColors
|
||||
from app.lib.playlistlib import ValidatePlaylistThumbs
|
||||
from app.lib.populate import CreateAlbums
|
||||
from app.lib.populate import Populate
|
||||
from app.logger import get_logger
|
||||
from PIL import Image
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.lib.trackslib import create_all_tracks
|
||||
log = get_logger()
|
||||
|
||||
|
||||
@helpers.background
|
||||
def reindex_tracks():
|
||||
def run_checks():
|
||||
"""
|
||||
Checks for new songs every 5 minutes.
|
||||
"""
|
||||
ValidateAlbumThumbs()
|
||||
|
||||
while True:
|
||||
populate()
|
||||
CheckArtistImages()()
|
||||
trackslib.validate_tracks()
|
||||
|
||||
time.sleep(60)
|
||||
Populate()
|
||||
CreateAlbums()
|
||||
|
||||
if helpers.Ping()():
|
||||
CheckArtistImages()()
|
||||
|
||||
@helpers.background
|
||||
def process_album_colors():
|
||||
ProcessAlbumColors()
|
||||
|
||||
ValidatePlaylistThumbs()
|
||||
process_album_colors()
|
||||
|
||||
time.sleep(300)
|
||||
|
||||
|
||||
@helpers.background
|
||||
@@ -38,15 +56,6 @@ def start_watchdog():
|
||||
watchdoge.watch.run()
|
||||
|
||||
|
||||
def populate():
|
||||
pop = Populate()
|
||||
pop.run()
|
||||
|
||||
tracks = create_all_tracks()
|
||||
api.TRACKS.clear()
|
||||
api.TRACKS.extend(tracks)
|
||||
|
||||
|
||||
class getArtistImage:
|
||||
"""
|
||||
Returns an artist image url.
|
||||
@@ -70,6 +79,7 @@ class getArtistImage:
|
||||
|
||||
|
||||
class useImageDownloader:
|
||||
|
||||
def __init__(self, url: str, dest: str) -> None:
|
||||
self.url = url
|
||||
self.dest = dest
|
||||
@@ -79,15 +89,18 @@ class useImageDownloader:
|
||||
img = Image.open(BytesIO(requests.get(self.url).content))
|
||||
img.save(self.dest, format="webp")
|
||||
img.close()
|
||||
return "fetched image"
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("🔴🔴🔴🔴🔴🔴🔴")
|
||||
time.sleep(5)
|
||||
return "connection error"
|
||||
|
||||
|
||||
class CheckArtistImages:
|
||||
|
||||
def __init__(self):
|
||||
self.artists: list[str] = []
|
||||
print("Checking for artist images")
|
||||
log.info("Checking artist images")
|
||||
|
||||
@staticmethod
|
||||
def check_if_exists(img_path: str):
|
||||
@@ -100,18 +113,6 @@ class CheckArtistImages:
|
||||
else:
|
||||
return False
|
||||
|
||||
def gather_artists(self):
|
||||
"""
|
||||
Loops through all the tracks and gathers all the artists.
|
||||
"""
|
||||
|
||||
for song in api.DB_TRACKS:
|
||||
this_artists: list = song["artists"].split(", ")
|
||||
|
||||
for artist in this_artists:
|
||||
if artist not in self.artists:
|
||||
self.artists.append(artist)
|
||||
|
||||
@classmethod
|
||||
def download_image(cls, artistname: str):
|
||||
"""
|
||||
@@ -120,28 +121,25 @@ class CheckArtistImages:
|
||||
:param artistname: The artist name
|
||||
"""
|
||||
|
||||
img_path = (
|
||||
helpers.app_dir
|
||||
+ "/images/artists/"
|
||||
+ artistname.replace("/", "::")
|
||||
+ ".webp"
|
||||
)
|
||||
img_path = (settings.APP_DIR + "/images/artists/" +
|
||||
helpers.create_safe_name(artistname) + ".webp")
|
||||
|
||||
if cls.check_if_exists(img_path):
|
||||
return
|
||||
return "exists"
|
||||
|
||||
url = getArtistImage(artistname)()
|
||||
|
||||
if url is None:
|
||||
return
|
||||
return "url is none"
|
||||
|
||||
useImageDownloader(url, img_path)()
|
||||
return useImageDownloader(url, img_path)()
|
||||
|
||||
def __call__(self):
|
||||
self.gather_artists()
|
||||
self.artists = helpers.Get.get_all_artists()
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
pool.map(self.download_image, self.artists)
|
||||
iter = pool.map(self.download_image, self.artists)
|
||||
[i for i in iter]
|
||||
|
||||
print("Done fetching images")
|
||||
|
||||
@@ -151,8 +149,7 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None:
|
||||
Returns the album bio for a given album.
|
||||
"""
|
||||
last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={}&format=json".format(
|
||||
settings.LAST_FM_API_KEY, albumartist, title
|
||||
)
|
||||
settings.LAST_FM_API_KEY, albumartist, title)
|
||||
|
||||
try:
|
||||
response = requests.get(last_fm_url)
|
||||
@@ -161,7 +158,8 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None:
|
||||
return None
|
||||
|
||||
try:
|
||||
bio = data["album"]["wiki"]["summary"].split('<a href="https://www.last.fm/')[0]
|
||||
bio = data["album"]["wiki"]["summary"].split(
|
||||
'<a href="https://www.last.fm/')[0]
|
||||
except KeyError:
|
||||
bio = None
|
||||
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
This module contains mini functions for the server.
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Set
|
||||
|
||||
import requests
|
||||
from app import instances
|
||||
from app import models
|
||||
from app import settings
|
||||
|
||||
app_dir = settings.APP_DIR
|
||||
|
||||
|
||||
def background(func):
|
||||
@@ -51,36 +50,16 @@ def run_fast_scandir(__dir: str, full=False) -> Dict[List[str], List[str]]:
|
||||
return subfolders, files
|
||||
|
||||
|
||||
def remove_duplicates(tracklist: List[models.Track]) -> List[models.Track]:
|
||||
"""
|
||||
Removes duplicates from a list. Returns a list without duplicates.
|
||||
"""
|
||||
class RemoveDuplicates:
|
||||
|
||||
song_num = 0
|
||||
def __init__(self, tracklist: List[models.Track]) -> None:
|
||||
self.tracklist = tracklist
|
||||
|
||||
while song_num < len(tracklist) - 1:
|
||||
for index, song in enumerate(tracklist):
|
||||
if (
|
||||
tracklist[song_num].title == song.title
|
||||
and tracklist[song_num].album == song.album
|
||||
and tracklist[song_num].artists == song.artists
|
||||
and index != song_num
|
||||
):
|
||||
tracklist.remove(song)
|
||||
def __call__(self) -> List[models.Track]:
|
||||
uniq_hashes = set(t.uniq_hash for t in self.tracklist)
|
||||
tracks = UseBisection(self.tracklist, "uniq_hash", uniq_hashes)()
|
||||
|
||||
song_num += 1
|
||||
|
||||
return tracklist
|
||||
|
||||
|
||||
# def save_image(url: str, path: str) -> None:
|
||||
# """
|
||||
# Saves an image from an url to a path.
|
||||
# """
|
||||
|
||||
# response = requests.get(url)
|
||||
# img = Image.open(BytesIO(response.content))
|
||||
# img.save(path, "JPEG")
|
||||
return tracks
|
||||
|
||||
|
||||
def is_valid_file(filename: str) -> bool:
|
||||
@@ -94,31 +73,13 @@ def is_valid_file(filename: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def use_memoji():
|
||||
"""
|
||||
Returns a path to a random memoji image.
|
||||
"""
|
||||
path = str(random.randint(0, 20)) + ".svg"
|
||||
return "defaults/" + path
|
||||
|
||||
|
||||
def check_artist_image(image: str) -> str:
|
||||
"""
|
||||
Checks if the artist image is valid.
|
||||
"""
|
||||
img_name = image.replace("/", "::") + ".webp"
|
||||
|
||||
if not os.path.exists(os.path.join(app_dir, "images", "artists", img_name)):
|
||||
return use_memoji()
|
||||
else:
|
||||
return img_name
|
||||
|
||||
|
||||
def create_album_hash(title: str, artist: str) -> str:
|
||||
"""
|
||||
Creates a simple hash for an album
|
||||
"""
|
||||
return (title + artist).replace(" ", "").lower()
|
||||
lower = (title + artist).replace(" ", "").lower()
|
||||
hash = "".join([i for i in lower if i.isalnum()])
|
||||
return hash
|
||||
|
||||
|
||||
def create_new_date():
|
||||
@@ -131,33 +92,33 @@ def create_safe_name(name: str) -> str:
|
||||
"""
|
||||
Creates a url-safe name from a name.
|
||||
"""
|
||||
return "".join([i for i in name if i not in '/\\:*?"<>|'])
|
||||
return "".join([i for i in name if i.isalnum()])
|
||||
|
||||
|
||||
class UseBisection:
|
||||
"""
|
||||
Uses bisection to find a list of items in another list.
|
||||
Uses bisection to find a list of items in another list.
|
||||
|
||||
returns a list of found items with `None` items being not found
|
||||
returns a list of found items with `None` items being not found
|
||||
items.
|
||||
"""
|
||||
|
||||
def __init__(self, source: List, search_from: str, queries: List[str]) -> None:
|
||||
self.list = source
|
||||
self.queries = queries
|
||||
self.search_from = search_from
|
||||
self.list.sort(key=lambda x: getattr(x, search_from))
|
||||
def __init__(self, source: List, search_from: str,
|
||||
queries: List[str]) -> None:
|
||||
self.source_list = source
|
||||
self.queries_list = queries
|
||||
self.attr = search_from
|
||||
self.source_list.sort(key=lambda x: getattr(x, search_from))
|
||||
|
||||
def find(self, query: str):
|
||||
left = 0
|
||||
right = len(self.list) - 1
|
||||
right = len(self.source_list) - 1
|
||||
|
||||
while left <= right:
|
||||
mid = (left + right) // 2
|
||||
|
||||
if self.list[mid].__getattribute__(self.search_from) == query:
|
||||
return self.list[mid]
|
||||
elif self.list[mid].__getattribute__(self.search_from) > query:
|
||||
if self.source_list[mid].__getattribute__(self.attr) == query:
|
||||
return self.source_list[mid]
|
||||
elif self.source_list[mid].__getattribute__(self.attr) > query:
|
||||
right = mid - 1
|
||||
else:
|
||||
left = mid + 1
|
||||
@@ -165,4 +126,57 @@ class UseBisection:
|
||||
return None
|
||||
|
||||
def __call__(self) -> List:
|
||||
return [self.find(query) for query in self.queries]
|
||||
if len(self.source_list) == 0:
|
||||
print("🚀🚀🚀🚀🚀🚀🚀")
|
||||
return [None]
|
||||
|
||||
return [self.find(query) for query in self.queries_list]
|
||||
|
||||
|
||||
class Get:
|
||||
|
||||
@staticmethod
|
||||
def get_all_tracks() -> List[models.Track]:
|
||||
"""
|
||||
Returns all tracks
|
||||
"""
|
||||
t = instances.tracks_instance.get_all_tracks()
|
||||
return [models.Track(t) for t in t]
|
||||
|
||||
def get_all_albums() -> List[models.Album]:
|
||||
"""
|
||||
Returns all albums
|
||||
"""
|
||||
a = instances.album_instance.get_all_albums()
|
||||
return [models.Album(a) for a in a]
|
||||
|
||||
@classmethod
|
||||
def get_all_artists(cls) -> Set[str]:
|
||||
tracks = cls.get_all_tracks()
|
||||
artists: Set[str] = set()
|
||||
|
||||
for track in tracks:
|
||||
for artist in track.artists:
|
||||
artists.add(artist.lower())
|
||||
|
||||
return artists
|
||||
|
||||
@staticmethod
|
||||
def get_all_playlists() -> List[models.Playlist]:
|
||||
"""
|
||||
Returns all playlists
|
||||
"""
|
||||
p = instances.playlist_instance.get_all_playlists()
|
||||
return [models.Playlist(p) for p in p]
|
||||
|
||||
|
||||
class Ping:
|
||||
"""Checks if there is a connection to the internet by pinging google.com"""
|
||||
|
||||
@staticmethod
|
||||
def __call__() -> bool:
|
||||
try:
|
||||
requests.get("https://google.com", timeout=10)
|
||||
return True
|
||||
except (requests.exceptions.ConnectionError, requests.Timeout):
|
||||
return False
|
||||
|
||||
@@ -12,11 +12,13 @@ def join(*args: Tuple[str]) -> str:
|
||||
|
||||
|
||||
HOME = path.expanduser("~")
|
||||
ROOT_PATH = path.join(HOME, ".alice", "images")
|
||||
APP_DIR = join(HOME, ".alice")
|
||||
IMG_PATH = path.join(APP_DIR, "images")
|
||||
|
||||
THUMB_PATH = join(ROOT_PATH, "thumbnails")
|
||||
ARTIST_PATH = join(ROOT_PATH, "artists")
|
||||
PLAYLIST_PATH = join(ROOT_PATH, "playlists")
|
||||
ASSETS_PATH = join(APP_DIR, "assets")
|
||||
THUMB_PATH = join(IMG_PATH, "thumbnails")
|
||||
ARTIST_PATH = join(IMG_PATH, "artists")
|
||||
PLAYLIST_PATH = join(IMG_PATH, "playlists")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@@ -24,6 +26,16 @@ def hello():
|
||||
return "Hello mf"
|
||||
|
||||
|
||||
def send_fallback_img():
|
||||
img = join(ASSETS_PATH, "default.webp")
|
||||
exists = path.exists(img)
|
||||
|
||||
if not exists:
|
||||
return "", 404
|
||||
|
||||
return send_from_directory(ASSETS_PATH, "default.webp")
|
||||
|
||||
|
||||
@app.route("/t/<imgpath>")
|
||||
def send_thumbnail(imgpath: str):
|
||||
fpath = join(THUMB_PATH, imgpath)
|
||||
@@ -32,7 +44,7 @@ def send_thumbnail(imgpath: str):
|
||||
if exists:
|
||||
return send_from_directory(THUMB_PATH, imgpath)
|
||||
|
||||
return {"msg": "Not found"}, 404
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
@app.route("/a/<imgpath>")
|
||||
@@ -43,7 +55,7 @@ def send_artist_image(imgpath: str):
|
||||
if exists:
|
||||
return send_from_directory(ARTIST_PATH, imgpath)
|
||||
|
||||
return {"msg": "Not found"}, 404
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
@app.route("/p/<imgpath>")
|
||||
@@ -54,11 +66,8 @@ def send_playlist_image(imgpath: str):
|
||||
if exists:
|
||||
return send_from_directory(PLAYLIST_PATH, imgpath)
|
||||
|
||||
return {"msg": "Not found"}, 404
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
# TODO
|
||||
# Return Fallback images instead of JSON
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(threaded=True, port=9877)
|
||||
|
||||
@@ -1,89 +1,93 @@
|
||||
"""
|
||||
This library contains all the functions related to albums.
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from app import api
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.lib import taglib
|
||||
from app.lib import trackslib
|
||||
from app.logger import logg
|
||||
from app.settings import THUMBS_PATH
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def get_all_albums() -> List[models.Album]:
|
||||
@dataclass
|
||||
class Thumbnail:
|
||||
filename: str
|
||||
|
||||
|
||||
class RipAlbumImage:
|
||||
"""
|
||||
Returns a list of album objects for all albums in the database.
|
||||
"""
|
||||
print("Getting all albums...")
|
||||
|
||||
albums: List[models.Album] = []
|
||||
|
||||
db_albums = instances.album_instance.get_all_albums()
|
||||
|
||||
for album in tqdm(db_albums, desc="Creating albums"):
|
||||
aa = models.Album(album)
|
||||
albums.append(aa)
|
||||
|
||||
return albums
|
||||
|
||||
|
||||
def create_everything() -> List[models.Track]:
|
||||
"""
|
||||
Creates album objects for all albums and returns
|
||||
a list of track objects
|
||||
"""
|
||||
albums: list[models.Album] = get_all_albums()
|
||||
|
||||
api.ALBUMS = albums
|
||||
api.ALBUMS.sort(key=lambda x: x.hash)
|
||||
|
||||
tracks = trackslib.create_all_tracks()
|
||||
|
||||
api.TRACKS.clear()
|
||||
api.TRACKS.extend(tracks)
|
||||
api.TRACKS.sort(key=lambda x: x.title)
|
||||
|
||||
|
||||
def find_album(albums: List[models.Album], hash: str) -> int | None:
|
||||
"""
|
||||
Finds an album by album title and artist.
|
||||
|
||||
:param `albums`: List of album objects.
|
||||
:param `hash`: Hash of album.
|
||||
:return: Index of album in list.
|
||||
Rips a thumbnail for the given album hash.
|
||||
"""
|
||||
|
||||
left = 0
|
||||
right = len(albums) - 1
|
||||
def __init__(self, hash: str) -> None:
|
||||
tracks = instances.tracks_instance.find_tracks_by_hash(hash)
|
||||
tracks = [models.Track(track) for track in tracks]
|
||||
|
||||
while left <= right:
|
||||
mid = (left + right) // 2
|
||||
for track in tracks:
|
||||
ripped = taglib.extract_thumb(track.filepath, hash + ".webp")
|
||||
|
||||
if albums[mid].hash == hash:
|
||||
return mid
|
||||
|
||||
if albums[mid].hash < hash:
|
||||
left = mid + 1
|
||||
else:
|
||||
right = mid - 1
|
||||
|
||||
return None
|
||||
if ripped:
|
||||
break
|
||||
|
||||
|
||||
def get_album_duration(album: List[models.Track]) -> int:
|
||||
"""
|
||||
Gets the duration of an album.
|
||||
"""
|
||||
class ValidateAlbumThumbs:
|
||||
|
||||
album_duration = 0
|
||||
@staticmethod
|
||||
def remove_obsolete():
|
||||
"""
|
||||
Removes unreferenced thumbnails from the thumbnails folder.
|
||||
"""
|
||||
entries = os.scandir(THUMBS_PATH)
|
||||
entries = [entry for entry in entries if entry.is_file()]
|
||||
|
||||
for track in album:
|
||||
album_duration += track.length
|
||||
albums = helpers.Get.get_all_albums()
|
||||
thumbs = [Thumbnail(album.hash + ".webp") for album in albums]
|
||||
|
||||
return album_duration
|
||||
for entry in tqdm(entries, desc="Validating thumbnails"):
|
||||
e = helpers.UseBisection(thumbs, "filename", [entry.name])()
|
||||
|
||||
if e is None:
|
||||
os.remove(entry.path)
|
||||
|
||||
@staticmethod
|
||||
def find_lost_thumbnails():
|
||||
"""
|
||||
Re-rip lost album thumbnails
|
||||
"""
|
||||
entries = os.scandir(THUMBS_PATH)
|
||||
entries = [
|
||||
Thumbnail(entry.name) for entry in entries if entry.is_file()
|
||||
]
|
||||
|
||||
albums = helpers.Get.get_all_albums()
|
||||
thumbs = [(album.hash + ".webp") for album in albums]
|
||||
|
||||
def rip_image(t_hash: str):
|
||||
e = helpers.UseBisection(entries, "filename", [t_hash])()[0]
|
||||
|
||||
if e is None:
|
||||
hash = t_hash.replace(".webp", "")
|
||||
RipAlbumImage(hash)
|
||||
|
||||
logg.info("Ripping lost album thumbnails")
|
||||
# with ThreadPoolExecutor() as pool:
|
||||
# i = pool.map(rip_image, thumbs)
|
||||
# [a for a in i]
|
||||
# ⚠️ empty lists are sent to the useBisection function as the source list.
|
||||
for thumb in thumbs:
|
||||
rip_image(thumb)
|
||||
|
||||
logg.info("Ripping lost album thumbnails ... ✅")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.remove_obsolete()
|
||||
self.find_lost_thumbnails()
|
||||
|
||||
|
||||
def use_defaults() -> str:
|
||||
@@ -94,31 +98,19 @@ def use_defaults() -> str:
|
||||
return path
|
||||
|
||||
|
||||
def gen_random_path() -> str:
|
||||
"""
|
||||
Generates a random image file path for an album image.
|
||||
"""
|
||||
choices = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
path = "".join(random.choice(choices) for i in range(20))
|
||||
path += ".webp"
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def get_album_image(album: list) -> str:
|
||||
def get_album_image(track: models.Track) -> str:
|
||||
"""
|
||||
Gets the image of an album.
|
||||
"""
|
||||
|
||||
for track in album:
|
||||
img_p = gen_random_path()
|
||||
img_p = track.albumhash + ".webp"
|
||||
|
||||
exists = taglib.extract_thumb(track["filepath"], webp_path=img_p)
|
||||
success = taglib.extract_thumb(track.filepath, webp_path=img_p)
|
||||
|
||||
if exists:
|
||||
return img_p
|
||||
if success:
|
||||
return img_p
|
||||
|
||||
return use_defaults()
|
||||
return None
|
||||
|
||||
|
||||
class GetAlbumTracks:
|
||||
@@ -127,66 +119,38 @@ class GetAlbumTracks:
|
||||
and album artist.
|
||||
"""
|
||||
|
||||
def __init__(self, tracklist: list, albumhash: str) -> None:
|
||||
def __init__(self, tracklist: List[models.Track], albumhash: str) -> None:
|
||||
self.hash = albumhash
|
||||
self.tracks = tracklist
|
||||
self.tracks.sort(key=lambda x: x["albumhash"])
|
||||
self.tracks.sort(key=lambda x: x.albumhash)
|
||||
|
||||
def find_tracks(self):
|
||||
tracks = []
|
||||
index = trackslib.find_track(self.tracks, self.hash)
|
||||
def __call__(self):
|
||||
tracks = helpers.UseBisection(self.tracks, "albumhash", [self.hash])()
|
||||
|
||||
while index is not None:
|
||||
track = self.tracks[index]
|
||||
tracks.append(track)
|
||||
self.tracks.remove(track)
|
||||
index = trackslib.find_track(self.tracks, self.hash)
|
||||
|
||||
# self.tracks.extend(tracks)
|
||||
# self.tracks.sort(key=lambda x: x["albumhash"])
|
||||
return tracks
|
||||
|
||||
|
||||
def get_album_tracks(album: str, artist: str) -> List:
|
||||
return GetAlbumTracks(album, artist).find_tracks()
|
||||
def get_album_tracks(tracklist: List[models.Track], hash: str) -> List:
|
||||
return GetAlbumTracks(tracklist, hash)()
|
||||
|
||||
|
||||
def create_album(track: dict, tracklist: list) -> dict:
|
||||
def create_album(track: models.Track) -> dict:
|
||||
"""
|
||||
Generates and returns an album object from a track object.
|
||||
"""
|
||||
album = {
|
||||
"title": track["album"],
|
||||
"artist": track["albumartist"],
|
||||
"title": track.album,
|
||||
"artist": track.albumartist,
|
||||
"hash": track.albumhash,
|
||||
}
|
||||
|
||||
albumhash = helpers.create_album_hash(album["title"], album["artist"])
|
||||
album_tracks = get_album_tracks(tracklist, albumhash)
|
||||
album["date"] = track.date
|
||||
|
||||
if len(album_tracks) == 0:
|
||||
return None
|
||||
img_p = get_album_image(track)
|
||||
|
||||
album["date"] = album_tracks[0]["date"]
|
||||
|
||||
album["image"] = get_album_image(album_tracks)
|
||||
# album["image"] = "".join(x for x in albumhash if x not in "\/:*?<>|")
|
||||
if img_p is not None:
|
||||
album["image"] = img_p
|
||||
return album
|
||||
|
||||
album["image"] = None
|
||||
return album
|
||||
|
||||
|
||||
def search_albums_by_name(query: str) -> List[models.Album]:
|
||||
"""
|
||||
Searches albums by album name.
|
||||
"""
|
||||
title_albums: List[models.Album] = []
|
||||
artist_albums: List[models.Album] = []
|
||||
|
||||
for album in api.ALBUMS:
|
||||
if query.lower() in album.title.lower():
|
||||
title_albums.append(album)
|
||||
|
||||
for album in api.ALBUMS:
|
||||
if query.lower() in album.artist.lower():
|
||||
artist_albums.append(album)
|
||||
|
||||
return [*title_albums, *artist_albums]
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
from io import BytesIO
|
||||
|
||||
import colorgram
|
||||
from app import api
|
||||
from app import instances
|
||||
from app.lib.taglib import return_album_art
|
||||
from PIL import Image
|
||||
from progress.bar import Bar
|
||||
from app import settings
|
||||
from app.helpers import Get
|
||||
from app.logger import get_logger
|
||||
from app.models import Album
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
def get_image_colors(image) -> list:
|
||||
def get_image_colors(image: str) -> list:
|
||||
"""Extracts 2 of the most dominant colors from an image."""
|
||||
try:
|
||||
colors = sorted(colorgram.extract(image, 2), key=lambda c: c.hsl.h)
|
||||
colors = sorted(colorgram.extract(image, 4), key=lambda c: c.hsl.h)
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
@@ -24,30 +24,26 @@ def get_image_colors(image) -> list:
|
||||
return formatted_colors
|
||||
|
||||
|
||||
def save_track_colors(img, filepath) -> None:
|
||||
"""Saves the track colors to the database"""
|
||||
class ProcessAlbumColors:
|
||||
|
||||
track_colors = get_image_colors(img)
|
||||
def __init__(self) -> None:
|
||||
log.info("Processing album colors")
|
||||
all_albums = Get.get_all_albums()
|
||||
|
||||
tc_dict = {
|
||||
"filepath": filepath,
|
||||
"colors": track_colors,
|
||||
}
|
||||
all_albums = [a for a in all_albums if len(a.colors) == 0]
|
||||
|
||||
instances.track_color_instance.insert_track_color(tc_dict)
|
||||
for a in all_albums:
|
||||
self.process_color(a)
|
||||
|
||||
log.info("Processing album colors ... ✅")
|
||||
|
||||
def save_t_colors():
|
||||
_bar = Bar("Processing image colors", max=len(api.DB_TRACKS))
|
||||
@staticmethod
|
||||
def process_color(album: Album):
|
||||
img = settings.THUMBS_PATH + "/" + album.image
|
||||
|
||||
for track in api.DB_TRACKS:
|
||||
filepath = track["filepath"]
|
||||
album_art = return_album_art(filepath)
|
||||
colors = get_image_colors(img)
|
||||
|
||||
if album_art is not None:
|
||||
img = Image.open(BytesIO(album_art))
|
||||
save_track_colors(img, filepath)
|
||||
if len(colors) > 0:
|
||||
instances.album_instance.set_album_colors(colors, album.hash)
|
||||
|
||||
_bar.next()
|
||||
|
||||
_bar.finish()
|
||||
return colors
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from os import scandir
|
||||
from time import time
|
||||
from typing import List
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
from app import api
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from app.models import Folder
|
||||
from app.models import Track
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -18,20 +15,12 @@ class Dir:
|
||||
is_sym: bool
|
||||
|
||||
|
||||
def get_valid_folders() -> None:
|
||||
for track in api.TRACKS:
|
||||
api.VALID_FOLDERS.add(track.folder)
|
||||
|
||||
|
||||
def get_folder_track_count(foldername: str) -> int:
|
||||
def get_folder_track_count(path: str) -> int:
|
||||
"""
|
||||
Returns the number of files associated with a folder.
|
||||
"""
|
||||
count = 0
|
||||
for track in api.TRACKS:
|
||||
if foldername in track.folder:
|
||||
count += 1
|
||||
return count
|
||||
tracks = instances.tracks_instance.get_dir_t_count(path)
|
||||
return len(tracks)
|
||||
|
||||
|
||||
def create_folder(dir: Dir) -> Folder:
|
||||
@@ -40,57 +29,12 @@ def create_folder(dir: Dir) -> Folder:
|
||||
"name": dir.path.split("/")[-1],
|
||||
"path": dir.path,
|
||||
"is_sym": dir.is_sym,
|
||||
"trackcount": get_folder_track_count(dir.path),
|
||||
"trackcount": instances.tracks_instance.get_dir_t_count(dir.path),
|
||||
}
|
||||
|
||||
return Folder(folder)
|
||||
|
||||
|
||||
def create_all_folders() -> Set[Folder]:
|
||||
folders: List[Folder] = []
|
||||
|
||||
for foldername in tqdm(api.VALID_FOLDERS, desc="Creating folders"):
|
||||
folder = create_folder(foldername)
|
||||
folders.append(folder)
|
||||
|
||||
return folders
|
||||
|
||||
|
||||
def get_subdirs(foldername: str) -> List[Folder]:
|
||||
"""
|
||||
Finds and Creates Folder objects for each sub-directory string in the foldername passed.
|
||||
"""
|
||||
subdirs = set()
|
||||
|
||||
for folder in api.VALID_FOLDERS:
|
||||
if foldername in folder:
|
||||
str0 = folder.replace(foldername, "")
|
||||
|
||||
try:
|
||||
str1 = str0.split("/")[1]
|
||||
except IndexError:
|
||||
str1 = None
|
||||
|
||||
if str1 is not None:
|
||||
subdirs.add(foldername + "/" + str1)
|
||||
return [create_folder(dir) for dir in subdirs]
|
||||
|
||||
|
||||
@helpers.background
|
||||
def run_scandir():
|
||||
"""
|
||||
Initiates the creation of all folder objects for each folder with a track in it.
|
||||
|
||||
Runs in a background thread after every 5 minutes.
|
||||
It calls the
|
||||
"""
|
||||
get_valid_folders()
|
||||
# folders_ = create_all_folders()
|
||||
"""Create all the folder objects before clearing api.FOLDERS"""
|
||||
|
||||
# api.FOLDERS = folders_
|
||||
|
||||
|
||||
class getFnF:
|
||||
"""
|
||||
Get files and folders from a directory.
|
||||
@@ -99,15 +43,6 @@ class getFnF:
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path = path
|
||||
|
||||
@classmethod
|
||||
def get_tracks(cls, files: List[str]) -> List[Track]:
|
||||
"""
|
||||
Returns a list of Track objects for each file in the given list.
|
||||
"""
|
||||
tracks = helpers.UseBisection(api.TRACKS, "filepath", files)()
|
||||
tracks = filter(lambda t: t is not None, tracks)
|
||||
return list(tracks)
|
||||
|
||||
def __call__(self) -> Tuple[Track, Folder]:
|
||||
try:
|
||||
all = scandir(self.path)
|
||||
@@ -125,9 +60,14 @@ class getFnF:
|
||||
dirs.append(Dir(**dir))
|
||||
elif entry.is_file() and entry.name.endswith((".mp3", ".flac")):
|
||||
files.append(entry.path)
|
||||
tracks = self.get_tracks(files)
|
||||
|
||||
folders = [create_folder(dir) for dir in dirs]
|
||||
tracks = instances.tracks_instance.find_songs_by_filenames(files)
|
||||
tracks = [Track(track) for track in tracks]
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
iter = pool.map(create_folder, dirs)
|
||||
folders = [i for i in iter if i is not None]
|
||||
|
||||
folders = filter(lambda f: f.trackcount > 0, folders)
|
||||
|
||||
return tracks, folders
|
||||
|
||||
@@ -5,63 +5,46 @@ import os
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from app import api
|
||||
from app import exceptions
|
||||
from app import instances
|
||||
from app import models
|
||||
from app import settings
|
||||
from app.helpers import Get
|
||||
from app.lib import trackslib
|
||||
from app.logger import get_logger
|
||||
from PIL import Image
|
||||
from PIL import ImageSequence
|
||||
from progress.bar import Bar
|
||||
from werkzeug import datastructures
|
||||
|
||||
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
|
||||
|
||||
logg = get_logger()
|
||||
|
||||
|
||||
def add_track(playlistid: str, trackid: str):
|
||||
"""
|
||||
Adds a track to a playlist in the api.PLAYLISTS dict and to the database.
|
||||
Adds a track to a playlist to the database.
|
||||
"""
|
||||
for playlist in api.PLAYLISTS:
|
||||
if playlist.playlistid == playlistid:
|
||||
tt = trackslib.get_track_by_id(trackid)
|
||||
tt = instances.tracks_instance.get_track_by_id(trackid)
|
||||
|
||||
track = {
|
||||
"title": tt.title,
|
||||
"artists": tt.artists,
|
||||
"album": tt.album,
|
||||
}
|
||||
if tt is None:
|
||||
return
|
||||
|
||||
try:
|
||||
playlist.add_track(track)
|
||||
instances.playlist_instance.add_track_to_playlist(
|
||||
playlistid, track)
|
||||
return
|
||||
except TrackExistsInPlaylist as error:
|
||||
raise error
|
||||
track = models.Track(tt)
|
||||
|
||||
playlist = instances.playlist_instance.get_playlist_by_id(playlistid)
|
||||
|
||||
def get_playlist_tracks(pid: str):
|
||||
for p in api.PLAYLISTS:
|
||||
if p.playlistid == pid:
|
||||
return p.tracks
|
||||
track = {
|
||||
"title": track.title,
|
||||
"artists": tt["artists"],
|
||||
"album": track.album,
|
||||
}
|
||||
if track in playlist["pre_tracks"]:
|
||||
raise TrackExistsInPlaylist
|
||||
|
||||
|
||||
def create_all_playlists():
|
||||
"""
|
||||
Gets all playlists from the database.
|
||||
"""
|
||||
playlists = instances.playlist_instance.get_all_playlists()
|
||||
|
||||
|
||||
for playlist in tqdm(playlists, desc="Creating playlists"):
|
||||
api.PLAYLISTS.append(models.Playlist(playlist))
|
||||
|
||||
validate_images()
|
||||
instances.playlist_instance.add_track_to_playlist(playlistid, track)
|
||||
|
||||
|
||||
def create_thumbnail(image: any, img_path: str) -> str:
|
||||
@@ -113,26 +96,47 @@ def save_p_image(file: datastructures.FileStorage, pid: str):
|
||||
return img_path, thumb_path
|
||||
|
||||
|
||||
def validate_images():
|
||||
class ValidatePlaylistThumbs:
|
||||
"""
|
||||
Removes all unused images in the images/playlists folder.
|
||||
"""
|
||||
images = []
|
||||
|
||||
for playlist in api.PLAYLISTS:
|
||||
if playlist.image:
|
||||
img_path = playlist.image.split("/")[-1]
|
||||
thumb_path = playlist.thumb.split("/")[-1]
|
||||
def __init__(self) -> None:
|
||||
images = []
|
||||
playlists = Get.get_all_playlists()
|
||||
|
||||
images.append(img_path)
|
||||
images.append(thumb_path)
|
||||
logg.info("Validating playlist thumbnails")
|
||||
for playlist in playlists:
|
||||
if playlist.image:
|
||||
img_path = playlist.image.split("/")[-1]
|
||||
thumb_path = playlist.thumb.split("/")[-1]
|
||||
|
||||
p_path = os.path.join(settings.APP_DIR, "images", "playlists")
|
||||
images.append(img_path)
|
||||
images.append(thumb_path)
|
||||
|
||||
for image in os.listdir(p_path):
|
||||
if image not in images:
|
||||
os.remove(os.path.join(p_path, image))
|
||||
p_path = os.path.join(settings.APP_DIR, "images", "playlists")
|
||||
|
||||
for image in os.listdir(p_path):
|
||||
if image not in images:
|
||||
os.remove(os.path.join(p_path, image))
|
||||
|
||||
logg.info("Validating playlist thumbnails ... ✅")
|
||||
|
||||
|
||||
def create_new_date():
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def create_playlist_tracks(playlist_tracks: List) -> List[models.Track]:
|
||||
"""
|
||||
Creates a list of model.Track objects from a list of playlist track dicts.
|
||||
"""
|
||||
tracks: List[models.Track] = []
|
||||
|
||||
for t in playlist_tracks:
|
||||
track = trackslib.get_p_track(t)
|
||||
|
||||
if track is not None:
|
||||
tracks.append(models.Track(track))
|
||||
|
||||
return tracks
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import os
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from copy import deepcopy
|
||||
from multiprocessing import Pool
|
||||
from os import path
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from app import api
|
||||
from app import instances
|
||||
from app import settings
|
||||
from app.helpers import create_album_hash
|
||||
from app.helpers import Get
|
||||
from app.helpers import run_fast_scandir
|
||||
from app.instances import album_instance
|
||||
from app.helpers import UseBisection
|
||||
from app.instances import tracks_instance
|
||||
from app.lib import folderslib
|
||||
from app.lib.albumslib import create_album
|
||||
from app.lib.albumslib import find_album
|
||||
from app.lib.taglib import get_tags
|
||||
from app.lib.trackslib import find_track
|
||||
from app.logger import Log
|
||||
from app.logger import logg
|
||||
from app.models import Album
|
||||
from app.models import Track
|
||||
from tqdm import tqdm
|
||||
@@ -33,37 +28,14 @@ class Populate:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.files = []
|
||||
self.db_tracks = []
|
||||
self.tagged_tracks = []
|
||||
self.folders = set()
|
||||
self.pre_albums = []
|
||||
self.albums: List[Album] = []
|
||||
|
||||
self.files = run_fast_scandir(settings.HOME_DIR, full=True)[1]
|
||||
self.db_tracks = tracks_instance.get_all_tracks()
|
||||
self.tag_count = 0
|
||||
self.exist_count = 0
|
||||
|
||||
def run(self):
|
||||
self.check_untagged()
|
||||
self.get_all_tags()
|
||||
|
||||
if len(self.tagged_tracks) == 0:
|
||||
return
|
||||
|
||||
self.tagged_tracks.sort(key=lambda x: x["albumhash"])
|
||||
self.tracks = deepcopy(self.tagged_tracks)
|
||||
|
||||
self.pre_albums = self.create_pre_albums(self.tagged_tracks)
|
||||
self.create_albums(self.pre_albums)
|
||||
|
||||
self.albums.sort(key=lambda x: x.hash)
|
||||
api.ALBUMS.sort(key=lambda x: x.hash)
|
||||
|
||||
self.save_albums()
|
||||
self.create_tracks()
|
||||
# self.create_folders()
|
||||
self.tag_untagged()
|
||||
|
||||
def check_untagged(self):
|
||||
"""
|
||||
@@ -75,159 +47,111 @@ class Populate:
|
||||
if track["filepath"] in self.files:
|
||||
self.files.remove(track["filepath"])
|
||||
|
||||
Log(f"Found {len(self.files)} untagged tracks")
|
||||
|
||||
def process_tags(self, tags: dict):
|
||||
for t in tags:
|
||||
if t is None:
|
||||
continue
|
||||
|
||||
t["albumhash"] = create_album_hash(t["album"], t["albumartist"])
|
||||
self.tagged_tracks.append(t)
|
||||
api.DB_TRACKS.append(t)
|
||||
|
||||
self.folders.add(t["folder"])
|
||||
|
||||
def get_tags(self, file: str):
|
||||
tags = get_tags(file)
|
||||
|
||||
if tags is not None:
|
||||
folder = tags["folder"]
|
||||
self.folders.add(folder)
|
||||
|
||||
tags["albumhash"] = create_album_hash(tags["album"],
|
||||
tags["albumartist"])
|
||||
hash = create_album_hash(tags["album"], tags["albumartist"])
|
||||
tags["albumhash"] = hash
|
||||
self.tagged_tracks.append(tags)
|
||||
api.DB_TRACKS.append(tags)
|
||||
|
||||
def get_all_tags(self):
|
||||
def tag_untagged(self):
|
||||
"""
|
||||
Loops through all the untagged files and tags them.
|
||||
"""
|
||||
|
||||
s = time.time()
|
||||
print(f"Started tagging files")
|
||||
logg.info("Tagging untagged tracks...")
|
||||
with ThreadPoolExecutor() as executor:
|
||||
executor.map(self.get_tags, self.files)
|
||||
|
||||
# with Pool(maxtasksperchild=10, processes=10) as p:
|
||||
# tags = p.map(get_tags, tqdm(self.files))
|
||||
# self.process_tags(tags)
|
||||
if len(self.tagged_tracks) > 0:
|
||||
tracks_instance.insert_many(self.tagged_tracks)
|
||||
|
||||
# for t in tqdm(self.files):
|
||||
# self.get_tags(t)
|
||||
logg.info(f"Tagged {len(self.tagged_tracks)} tracks.")
|
||||
|
||||
d = time.time() - s
|
||||
Log(f"Tagged {len(self.tagged_tracks)} files in {d} seconds")
|
||||
|
||||
@dataclass
|
||||
class PreAlbum:
|
||||
title: str
|
||||
artist: str
|
||||
hash: str
|
||||
|
||||
|
||||
class CreateAlbums:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.db_tracks = Get.get_all_tracks()
|
||||
self.db_albums = Get.get_all_albums()
|
||||
|
||||
prealbums = self.create_pre_albums(self.db_tracks)
|
||||
prealbums = self.filter_processed(self.db_albums, prealbums)
|
||||
|
||||
albums = []
|
||||
|
||||
for album in tqdm(prealbums, desc="Creating albums"):
|
||||
a = self.create_album(album)
|
||||
if a is not None:
|
||||
albums.append(a)
|
||||
|
||||
# with ThreadPoolExecutor() as pool:
|
||||
# iterator = pool.map(self.create_album, prealbums)
|
||||
|
||||
# for i in iterator:
|
||||
# if i is not None:
|
||||
# albums.append(i)
|
||||
|
||||
if len(albums) > 0:
|
||||
instances.album_instance.insert_many(albums)
|
||||
|
||||
@staticmethod
|
||||
def create_pre_albums(tracks: List[dict]):
|
||||
"""
|
||||
Creates pre-albums for the all tagged tracks.
|
||||
"""
|
||||
def create_pre_albums(tracks: List[Track]) -> List[PreAlbum]:
|
||||
prealbums = []
|
||||
|
||||
for track in tqdm(tracks, desc="Creating pre-albums"):
|
||||
album = {"title": track["album"], "artist": track["albumartist"]}
|
||||
for track in tqdm(tracks, desc="Creating prealbums"):
|
||||
album = {
|
||||
"title": track.album,
|
||||
"artist": track.albumartist,
|
||||
"hash": track.albumhash,
|
||||
}
|
||||
|
||||
album = PreAlbum(**album)
|
||||
|
||||
if album not in prealbums:
|
||||
prealbums.append(album)
|
||||
|
||||
Log(f"Created {len(prealbums)} pre-albums")
|
||||
return prealbums
|
||||
|
||||
def create_album(self, album: dict):
|
||||
albumhash = create_album_hash(album["title"], album["artist"])
|
||||
index = find_album(api.ALBUMS, albumhash)
|
||||
@staticmethod
|
||||
def filter_processed(albums: List[Album],
|
||||
prealbums: List[PreAlbum]) -> List[dict]:
|
||||
to_process = []
|
||||
|
||||
if index is not None:
|
||||
album = api.ALBUMS[index]
|
||||
self.albums.append(album)
|
||||
for p in tqdm(prealbums, desc="Filtering processed albums"):
|
||||
album = UseBisection(albums, "hash", [p.hash])()[0]
|
||||
|
||||
self.exist_count += 1
|
||||
return
|
||||
if album is None:
|
||||
to_process.append(p)
|
||||
|
||||
self.albums.sort(key=lambda x: x.hash)
|
||||
index = find_track(self.tagged_tracks, albumhash)
|
||||
return to_process
|
||||
|
||||
if index is None:
|
||||
return
|
||||
def create_album(self, album: PreAlbum) -> Album:
|
||||
hash = album.hash
|
||||
|
||||
track = self.tagged_tracks[index]
|
||||
album = {"image": None}
|
||||
iter = 0
|
||||
|
||||
album = create_album(track, self.tagged_tracks)
|
||||
|
||||
if album is None:
|
||||
print("album is none")
|
||||
return
|
||||
|
||||
album = Album(album)
|
||||
|
||||
api.ALBUMS.append(album)
|
||||
self.albums.append(album)
|
||||
|
||||
def create_albums(self, albums: List[dict]):
|
||||
"""
|
||||
Uses the pre-albums to create new albums and add them to the database.
|
||||
"""
|
||||
for album in tqdm(albums, desc="Building albums"):
|
||||
self.create_album(album)
|
||||
|
||||
Log(f"{self.exist_count} of {len(albums)} albums were already in the database"
|
||||
)
|
||||
|
||||
def create_track(self, track: dict):
|
||||
"""
|
||||
Creates a single track object.
|
||||
"""
|
||||
|
||||
albumhash = track["albumhash"]
|
||||
index = find_album(self.albums, albumhash)
|
||||
|
||||
if index is None:
|
||||
return
|
||||
while album["image"] is None:
|
||||
track = UseBisection(self.db_tracks, "albumhash", [hash])()[0]
|
||||
|
||||
if track is not None:
|
||||
iter += 1
|
||||
album = create_album(track)
|
||||
self.db_tracks.remove(track)
|
||||
else:
|
||||
album["image"] = hash + ".webp"
|
||||
try:
|
||||
album: Album = self.albums[index]
|
||||
except (TypeError):
|
||||
"""
|
||||
😭😭😭
|
||||
"""
|
||||
pass
|
||||
|
||||
track["image"] = album.image
|
||||
|
||||
upsert_id = tracks_instance.insert_song(track)
|
||||
track["_id"] = {"$oid": str(upsert_id)}
|
||||
|
||||
api.TRACKS.append(Track(track))
|
||||
|
||||
def create_tracks(self):
|
||||
"""
|
||||
Loops through all the tagged tracks creating complete track objects using the `models.Track` model.
|
||||
"""
|
||||
with ThreadPoolExecutor() as executor:
|
||||
executor.map(self.create_track, self.tagged_tracks)
|
||||
|
||||
Log(f"Added {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums"
|
||||
)
|
||||
|
||||
def save_albums(self):
|
||||
"""
|
||||
Saves the albums to the database.
|
||||
"""
|
||||
|
||||
with ThreadPoolExecutor() as executor:
|
||||
executor.map(album_instance.insert_album, self.albums)
|
||||
|
||||
# def create_folders(self):
|
||||
# """
|
||||
# Creates the folder objects for all the tracks.
|
||||
# """
|
||||
# for folder in tqdm(self.folders, desc="Creating folders"):
|
||||
# api.VALID_FOLDERS.add(folder)
|
||||
|
||||
# fff = folderslib.create_folder(folder)
|
||||
# api.FOLDERS.append(fff)
|
||||
|
||||
# Log(f"Created {len(self.folders)} new folders")
|
||||
album = Album(album)
|
||||
return album
|
||||
except KeyError:
|
||||
print(f"📌 {iter}")
|
||||
print(album)
|
||||
|
||||
@@ -19,9 +19,10 @@ class Cutoff:
|
||||
Holds all the default cutoff values.
|
||||
"""
|
||||
|
||||
tracks: int = 70
|
||||
albums: int = 70
|
||||
artists: int = 70
|
||||
tracks: int = 80
|
||||
albums: int = 80
|
||||
artists: int = 80
|
||||
playlists: int = 80
|
||||
|
||||
|
||||
class Limit:
|
||||
@@ -32,19 +33,21 @@ class Limit:
|
||||
tracks: int = 50
|
||||
albums: int = 50
|
||||
artists: int = 50
|
||||
playlists: int = 50
|
||||
|
||||
|
||||
class SearchTracks:
|
||||
|
||||
def __init__(self, query) -> None:
|
||||
def __init__(self, tracks: List[models.Track], query: str) -> None:
|
||||
self.query = query
|
||||
self.tracks = tracks
|
||||
|
||||
def __call__(self) -> List[models.Track]:
|
||||
"""
|
||||
Gets all songs with a given title.
|
||||
"""
|
||||
|
||||
tracks = [track.title for track in api.TRACKS]
|
||||
tracks = [track.title for track in self.tracks]
|
||||
results = process.extract(
|
||||
self.query,
|
||||
tracks,
|
||||
@@ -53,39 +56,23 @@ class SearchTracks:
|
||||
limit=Limit.tracks,
|
||||
)
|
||||
|
||||
return [api.TRACKS[i[2]] for i in results]
|
||||
return [self.tracks[i[2]] for i in results]
|
||||
|
||||
|
||||
class SearchArtists:
|
||||
|
||||
def __init__(self, query) -> None:
|
||||
def __init__(self, artists: set[str], query: str) -> None:
|
||||
self.query = query
|
||||
|
||||
@staticmethod
|
||||
def get_all_artist_names() -> List[str]:
|
||||
"""
|
||||
Gets all artist names.
|
||||
"""
|
||||
|
||||
artists = [track.artists for track in api.TRACKS]
|
||||
|
||||
f_artists = set()
|
||||
|
||||
for artist in artists:
|
||||
for a in artist:
|
||||
f_artists.add(a)
|
||||
|
||||
return f_artists
|
||||
self.artists = artists
|
||||
|
||||
def __call__(self) -> list:
|
||||
"""
|
||||
Gets all artists with a given name.
|
||||
"""
|
||||
|
||||
artists = self.get_all_artist_names()
|
||||
results = process.extract(
|
||||
self.query,
|
||||
artists,
|
||||
self.artists,
|
||||
scorer=fuzz.WRatio,
|
||||
score_cutoff=Cutoff.artists,
|
||||
limit=Limit.artists,
|
||||
@@ -104,24 +91,17 @@ class SearchArtists:
|
||||
|
||||
class SearchAlbums:
|
||||
|
||||
def __init__(self, query) -> None:
|
||||
def __init__(self, albums: List[models.Album], query: str) -> None:
|
||||
self.query = query
|
||||
|
||||
def get_albums_by_name(self) -> List[models.Album]:
|
||||
"""
|
||||
Gets all albums with a given title.
|
||||
"""
|
||||
|
||||
albums = [album.title for album in api.ALBUMS]
|
||||
results = process.extract(self.query, albums)
|
||||
return [api.ALBUMS[i[2]] for i in results]
|
||||
self.albums = albums
|
||||
|
||||
def __call__(self) -> List[models.Album]:
|
||||
"""
|
||||
Gets all albums with a given title.
|
||||
"""
|
||||
|
||||
albums = [a.title for a in api.ALBUMS]
|
||||
albums = [a.title.lower() for a in self.albums]
|
||||
|
||||
results = process.extract(
|
||||
self.query,
|
||||
albums,
|
||||
@@ -130,7 +110,7 @@ class SearchAlbums:
|
||||
limit=Limit.albums,
|
||||
)
|
||||
|
||||
return [api.ALBUMS[i[2]] for i in results]
|
||||
return [self.albums[i[2]] for i in results]
|
||||
|
||||
# get all artists that matched the query
|
||||
# for get all albums from the artists
|
||||
@@ -140,31 +120,20 @@ class SearchAlbums:
|
||||
# recheck next and previous artist on play next or add to playlist
|
||||
|
||||
|
||||
class GetTopArtistTracks:
|
||||
class SearchPlaylists:
|
||||
|
||||
def __init__(self, artist: str) -> None:
|
||||
self.artist = artist
|
||||
def __init__(self, playlists: List[models.Playlist], query: str) -> None:
|
||||
self.playlists = playlists
|
||||
self.query = query
|
||||
|
||||
def __call__(self) -> List[models.Track]:
|
||||
"""
|
||||
Gets all tracks from a given artist.
|
||||
"""
|
||||
def __call__(self) -> List[models.Playlist]:
|
||||
playlists = [p.name for p in self.playlists]
|
||||
results = process.extract(
|
||||
self.query,
|
||||
playlists,
|
||||
scorer=fuzz.WRatio,
|
||||
score_cutoff=Cutoff.playlists,
|
||||
limit=Limit.playlists,
|
||||
)
|
||||
|
||||
return [track for track in api.TRACKS if self.artist in track.artists]
|
||||
|
||||
|
||||
def get_search_albums(query: str) -> List[models.Album]:
|
||||
"""
|
||||
Gets all songs with a given album.
|
||||
"""
|
||||
return albumslib.search_albums_by_name(query)
|
||||
|
||||
|
||||
def get_artists(artist: str) -> List[models.Track]:
|
||||
"""
|
||||
Gets all songs with a given artist.
|
||||
"""
|
||||
return [
|
||||
track for track in api.TRACKS
|
||||
if artist.lower() in str(track.artists).lower()
|
||||
]
|
||||
return [self.playlists[i[2]] for i in results]
|
||||
|
||||
@@ -33,6 +33,7 @@ def extract_thumb(filepath: str, webp_path: str) -> bool:
|
||||
Extracts the thumbnail from an audio file. Returns the path to the thumbnail.
|
||||
"""
|
||||
img_path = os.path.join(settings.THUMBS_PATH, webp_path)
|
||||
tsize = settings.THUMB_SIZE
|
||||
|
||||
if os.path.exists(img_path):
|
||||
return True
|
||||
@@ -43,12 +44,12 @@ def extract_thumb(filepath: str, webp_path: str) -> bool:
|
||||
img = Image.open(BytesIO(album_art))
|
||||
|
||||
try:
|
||||
small_img = img.resize((250, 250), Image.ANTIALIAS)
|
||||
small_img = img.resize((tsize, tsize), Image.ANTIALIAS)
|
||||
small_img.save(img_path, format="webp")
|
||||
except OSError:
|
||||
try:
|
||||
png = img.convert("RGB")
|
||||
small_img = png.resize((250, 250), Image.ANTIALIAS)
|
||||
small_img = png.resize((tsize, tsize), Image.ANTIALIAS)
|
||||
small_img.save(webp_path, format="webp")
|
||||
except:
|
||||
return False
|
||||
@@ -135,8 +136,8 @@ def parse_track_number(tags):
|
||||
Parses the track number from an audio file.
|
||||
"""
|
||||
try:
|
||||
track_number = tags["tracknumber"][0]
|
||||
except (KeyError, IndexError):
|
||||
track_number = int(tags["tracknumber"][0])
|
||||
except (KeyError, IndexError, ValueError):
|
||||
track_number = 1
|
||||
|
||||
return track_number
|
||||
@@ -147,8 +148,8 @@ def parse_disk_number(tags):
|
||||
Parses the disk number from an audio file.
|
||||
"""
|
||||
try:
|
||||
disk_number = tags["disknumber"][0]
|
||||
except (KeyError, IndexError):
|
||||
disk_number = int(tags["disknumber"][0])
|
||||
except (KeyError, IndexError, ValueError):
|
||||
disk_number = 1
|
||||
|
||||
return disk_number
|
||||
|
||||
@@ -2,74 +2,24 @@
|
||||
This library contains all the functions related to tracks.
|
||||
"""
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from app import api
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.helpers import remove_duplicates
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def create_all_tracks() -> List[models.Track]:
|
||||
def validate_tracks() -> None:
|
||||
"""
|
||||
Gets all songs under the ~/ directory.
|
||||
"""
|
||||
tracks: list[models.Track] = []
|
||||
entries = instances.tracks_instance.get_all_tracks()
|
||||
|
||||
for track in tqdm(api.DB_TRACKS, desc="Creating tracks"):
|
||||
for track in tqdm(entries, desc="Validating tracks"):
|
||||
try:
|
||||
os.chmod(track["filepath"], 0o755)
|
||||
except FileNotFoundError:
|
||||
instances.tracks_instance.remove_song_by_id(track["_id"]["$oid"])
|
||||
api.DB_TRACKS.remove(track)
|
||||
|
||||
tracks.append(models.Track(track))
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
def get_album_tracks(albumname, artist):
|
||||
"""Returns api tracks matching an album"""
|
||||
_tracks: List[models.Track] = []
|
||||
|
||||
for track in api.TRACKS:
|
||||
if track.album == albumname and track.albumartist == artist:
|
||||
_tracks.append(track)
|
||||
|
||||
return remove_duplicates(_tracks)
|
||||
|
||||
|
||||
def get_track_by_id(trackid: str) -> models.Track:
|
||||
"""Returns api track matching an id"""
|
||||
for track in api.TRACKS:
|
||||
try:
|
||||
if track.trackid == trackid:
|
||||
return track
|
||||
except AttributeError:
|
||||
print("AttributeError")
|
||||
print(track)
|
||||
|
||||
|
||||
def find_track(tracks: list, hash: str) -> int or None:
|
||||
"""
|
||||
Finds an album by album title and artist.
|
||||
"""
|
||||
|
||||
left = 0
|
||||
right = len(tracks) - 1
|
||||
iter = 0
|
||||
|
||||
while left <= right:
|
||||
iter += 1
|
||||
mid = (left + right) // 2
|
||||
|
||||
if tracks[mid]["albumhash"] == hash:
|
||||
return mid
|
||||
|
||||
if tracks[mid]["albumhash"] < hash:
|
||||
left = mid + 1
|
||||
else:
|
||||
right = mid - 1
|
||||
|
||||
return None
|
||||
def get_p_track(ptrack):
|
||||
return instances.tracks_instance.find_track_by_title_artists_album(
|
||||
ptrack["title"], ptrack["artists"], ptrack["album"])
|
||||
|
||||
@@ -4,16 +4,15 @@ This library contains the classes and functions related to the watchdog file wat
|
||||
import os
|
||||
import time
|
||||
|
||||
from app import api
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.helpers import UseBisection, create_album_hash
|
||||
from app.lib.albumslib import create_album
|
||||
from app.lib.albumslib import find_album
|
||||
from app.helpers import create_album_hash
|
||||
from app.lib.taglib import get_tags
|
||||
from app.logger import get_logger
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
class OnMyWatch:
|
||||
"""
|
||||
@@ -28,7 +27,12 @@ class OnMyWatch:
|
||||
def run(self):
|
||||
event_handler = Handler()
|
||||
self.observer.schedule(event_handler, self.directory, recursive=True)
|
||||
self.observer.start()
|
||||
|
||||
try:
|
||||
self.observer.start()
|
||||
except OSError:
|
||||
log.error("Could not start watchdog.")
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
@@ -51,24 +55,7 @@ def add_track(filepath: str) -> None:
|
||||
if tags is not None:
|
||||
hash = create_album_hash(tags["album"], tags["albumartist"])
|
||||
tags["albumhash"] = hash
|
||||
api.DB_TRACKS.append(tags)
|
||||
|
||||
albumindex = find_album(api.ALBUMS, hash)
|
||||
|
||||
if albumindex is not None:
|
||||
album = api.ALBUMS[albumindex]
|
||||
else:
|
||||
album_data = create_album(tags, api.DB_TRACKS)
|
||||
album = models.Album(album_data)
|
||||
|
||||
instances.album_instance.insert_album(album)
|
||||
api.ALBUMS.append(album)
|
||||
|
||||
tags["image"] = album.image
|
||||
upsert_id = instances.tracks_instance.insert_song(tags)
|
||||
tags["_id"] = {"$oid": str(upsert_id)}
|
||||
|
||||
api.TRACKS.append(models.Track(tags))
|
||||
instances.tracks_instance.insert_song(tags)
|
||||
|
||||
|
||||
def remove_track(filepath: str) -> None:
|
||||
@@ -76,16 +63,7 @@ def remove_track(filepath: str) -> None:
|
||||
Removes a track from the music dict.
|
||||
"""
|
||||
|
||||
try:
|
||||
trackid = instances.tracks_instance.get_song_by_path(filepath)["_id"]["$oid"]
|
||||
except TypeError:
|
||||
print(f"💙 Watchdog Error: Error removing track {filepath} TypeError")
|
||||
return
|
||||
|
||||
track = UseBisection(api.TRACKS, "trackid", [trackid])()
|
||||
if track is not None:
|
||||
api.TRACKS.remove(track[0])
|
||||
instances.tracks_instance.remove_song_by_id(trackid)
|
||||
instances.tracks_instance.remove_song_by_filepath(filepath)
|
||||
|
||||
|
||||
class Handler(PatternMatchingEventHandler):
|
||||
@@ -148,7 +126,3 @@ class Handler(PatternMatchingEventHandler):
|
||||
|
||||
|
||||
watch = OnMyWatch()
|
||||
|
||||
# TODO
|
||||
# When removing a track, check if there are other tracks in the same album,
|
||||
# if it was the last one, remove the album.
|
||||
@@ -1,8 +1,48 @@
|
||||
from app.settings import logger
|
||||
import logging
|
||||
|
||||
|
||||
class Log:
|
||||
class CustomFormatter(logging.Formatter):
|
||||
|
||||
def __init__(self, msg):
|
||||
if logger.enable:
|
||||
print("\n🦋 " + msg + "\n")
|
||||
grey = "\x1b[38;20m"
|
||||
yellow = "\x1b[33;20m"
|
||||
red = "\x1b[31;20m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
reset = "\x1b[0m"
|
||||
# format = (
|
||||
# "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
|
||||
# )
|
||||
format = "[%(asctime)s] [%(levelname)s] [@%(name)s]ℹ️ %(message)s"
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: grey + format + reset,
|
||||
logging.INFO: grey + format + reset,
|
||||
logging.WARNING: yellow + format + reset,
|
||||
logging.ERROR: red + format + reset,
|
||||
logging.CRITICAL: bold_red + format + reset,
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt, "%H:%M:%S")
|
||||
return formatter.format(record)
|
||||
|
||||
|
||||
logg = logging.getLogger("ALICE_MUSIC_SERVER")
|
||||
logg.setLevel(logging.DEBUG)
|
||||
|
||||
# create console handler with a higher log level
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
|
||||
ch.setFormatter(CustomFormatter())
|
||||
|
||||
logg.addHandler(ch)
|
||||
|
||||
|
||||
def get_logger():
|
||||
return logg
|
||||
|
||||
|
||||
logg = get_logger()
|
||||
|
||||
# copied from: https://stackoverflow.com/a/56944256:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""
|
||||
Contains all the models for objects generation and typing.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from typing import List
|
||||
|
||||
from app import api, helpers
|
||||
from app.exceptions import TrackExistsInPlaylist
|
||||
from app import helpers
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -16,7 +17,7 @@ class Track:
|
||||
|
||||
trackid: str
|
||||
title: str
|
||||
artists: list
|
||||
artists: list[str]
|
||||
albumartist: str
|
||||
album: str
|
||||
folder: str
|
||||
@@ -24,18 +25,15 @@ class Track:
|
||||
length: int
|
||||
genre: str
|
||||
bitrate: int
|
||||
image: str
|
||||
tracknumber: int
|
||||
disknumber: int
|
||||
albumhash: str
|
||||
date: str
|
||||
image: str
|
||||
uniq_hash: str
|
||||
|
||||
def __init__(self, tags):
|
||||
try:
|
||||
self.trackid = tags["_id"]["$oid"]
|
||||
except KeyError:
|
||||
print("No id")
|
||||
print(tags)
|
||||
|
||||
self.trackid = tags["_id"]["$oid"]
|
||||
self.title = tags["title"]
|
||||
self.artists = tags["artists"].split(", ")
|
||||
self.albumartist = tags["albumartist"]
|
||||
@@ -47,16 +45,17 @@ class Track:
|
||||
self.length = int(tags["length"])
|
||||
self.disknumber = int(tags["disknumber"])
|
||||
self.albumhash = tags["albumhash"]
|
||||
self.date = tags["date"]
|
||||
self.image = tags["albumhash"] + ".webp"
|
||||
self.tracknumber = int(tags["tracknumber"])
|
||||
|
||||
try:
|
||||
self.image = tags["image"]
|
||||
except KeyError:
|
||||
print(tags)
|
||||
self.uniq_hash = self.create_unique_hash("".join(self.artists),
|
||||
self.album, self.title)
|
||||
|
||||
try:
|
||||
self.tracknumber = int(tags["tracknumber"])
|
||||
except ValueError:
|
||||
self.tracknumber = 1
|
||||
@staticmethod
|
||||
def create_unique_hash(*args):
|
||||
string = "".join(str(a) for a in args).replace(" ", "")
|
||||
return "".join([i for i in string if i.isalnum()]).lower()
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -78,30 +77,32 @@ class Artist:
|
||||
@dataclass
|
||||
class Album:
|
||||
"""
|
||||
Album class
|
||||
Creates an album object
|
||||
"""
|
||||
|
||||
title: str
|
||||
artist: str
|
||||
hash: str
|
||||
date: int
|
||||
image: str
|
||||
hash: str
|
||||
count: int = 0
|
||||
duration: int = 0
|
||||
is_soundtrack: bool = False
|
||||
is_compilation: bool = False
|
||||
is_single: bool = False
|
||||
colors: List[str] = field(default_factory=list)
|
||||
|
||||
def __init__(self, tags):
|
||||
self.title = tags["title"]
|
||||
self.artist = tags["artist"]
|
||||
self.date = tags["date"]
|
||||
self.image = tags["image"]
|
||||
self.hash = tags["hash"]
|
||||
|
||||
try:
|
||||
self.hash = tags["albumhash"]
|
||||
self.colors = tags["colors"]
|
||||
except KeyError:
|
||||
self.hash = helpers.create_album_hash(self.title, self.artist)
|
||||
self.colors = []
|
||||
|
||||
@property
|
||||
def is_soundtrack(self) -> bool:
|
||||
@@ -117,30 +118,6 @@ class Album:
|
||||
return self.artist.lower() == "various artists"
|
||||
|
||||
|
||||
def get_p_track(ptrack):
|
||||
for track in api.TRACKS:
|
||||
if (
|
||||
track.title == ptrack["title"]
|
||||
and track.artists == ptrack["artists"]
|
||||
and ptrack["album"] == track.album
|
||||
):
|
||||
return track
|
||||
|
||||
|
||||
def create_playlist_tracks(playlist_tracks: List) -> List[Track]:
|
||||
"""
|
||||
Creates a list of model.Track objects from a list of playlist track dicts.
|
||||
"""
|
||||
tracks: List[Track] = []
|
||||
|
||||
for t in playlist_tracks:
|
||||
track = get_p_track(t)
|
||||
if track is not None:
|
||||
tracks.append(track)
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
@dataclass
|
||||
class Playlist:
|
||||
"""Creates playlist objects"""
|
||||
@@ -148,7 +125,7 @@ class Playlist:
|
||||
playlistid: str
|
||||
name: str
|
||||
tracks: List[Track]
|
||||
_pre_tracks: list = field(init=False, repr=False)
|
||||
pretracks: list = field(init=False, repr=False)
|
||||
lastUpdated: int
|
||||
image: str
|
||||
thumb: str
|
||||
@@ -162,16 +139,10 @@ class Playlist:
|
||||
self.description = data["description"]
|
||||
self.image = self.create_img_link(data["image"])
|
||||
self.thumb = self.create_img_link(data["thumb"])
|
||||
self._pre_tracks = data["pre_tracks"]
|
||||
self.pretracks = data["pre_tracks"]
|
||||
self.tracks = []
|
||||
self.lastUpdated = data["lastUpdated"]
|
||||
self.count = len(self._pre_tracks)
|
||||
|
||||
def get_tracks(self) -> List[Track]:
|
||||
"""
|
||||
Generates and returns Track objects from pre_tracks
|
||||
"""
|
||||
return create_playlist_tracks(self._pre_tracks)
|
||||
self.count = len(self.pretracks)
|
||||
|
||||
def create_img_link(self, image: str):
|
||||
if image:
|
||||
@@ -179,20 +150,6 @@ class Playlist:
|
||||
|
||||
return "default.webp"
|
||||
|
||||
def update_count(self):
|
||||
self.count = len(self._pre_tracks)
|
||||
|
||||
def add_track(self, track):
|
||||
if track not in self._pre_tracks:
|
||||
self._pre_tracks.append(track)
|
||||
self.update_count()
|
||||
self.lastUpdated = helpers.create_new_date()
|
||||
else:
|
||||
raise TrackExistsInPlaylist("Track already exists in playlist")
|
||||
|
||||
def update_desc(self, desc):
|
||||
self.description = desc
|
||||
|
||||
def update_playlist(self, data: dict):
|
||||
self.name = data["name"]
|
||||
self.description = data["description"]
|
||||
|
||||
@@ -2,10 +2,37 @@
|
||||
Contains the functions to prepare the server for use.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from app import settings
|
||||
|
||||
|
||||
class CopyFiles:
|
||||
"""Copies assets to the app directory."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
files = [{
|
||||
"src": "assets",
|
||||
"dest": os.path.join(settings.APP_DIR, "assets"),
|
||||
"is_dir": True,
|
||||
}]
|
||||
|
||||
for entry in files:
|
||||
src = os.path.join(os.getcwd(), entry["src"])
|
||||
|
||||
if entry["is_dir"]:
|
||||
shutil.copytree(
|
||||
src,
|
||||
entry["dest"],
|
||||
ignore=shutil.ignore_patterns("*.pyc", ),
|
||||
copy_function=shutil.copy2,
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
break
|
||||
|
||||
shutil.copy2(src, entry["dest"])
|
||||
|
||||
|
||||
def create_config_dir() -> None:
|
||||
"""
|
||||
Creates the config directory if it doesn't exist.
|
||||
@@ -29,3 +56,5 @@ def create_config_dir() -> None:
|
||||
if not exists:
|
||||
os.makedirs(path)
|
||||
os.chmod(path, 0o755)
|
||||
|
||||
CopyFiles()
|
||||
|
||||
@@ -59,6 +59,7 @@ class Playlist:
|
||||
lastUpdated: int
|
||||
description: str
|
||||
count: int = 0
|
||||
duration: int = 0
|
||||
|
||||
def __init__(self,
|
||||
p: models.Playlist,
|
||||
@@ -72,7 +73,8 @@ class Playlist:
|
||||
self.count = p.count
|
||||
|
||||
if construct_last_updated:
|
||||
self.lastUpdated = self.l_updated(p.lastUpdated)
|
||||
self.lastUpdated = self.get_l_updated(p.lastUpdated)
|
||||
|
||||
def l_updated(self, date: str) -> str:
|
||||
@staticmethod
|
||||
def get_l_updated(date: str) -> str:
|
||||
return date_string_to_time_passed(date)
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
"""
|
||||
Contains default configs
|
||||
"""
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
import multiprocessing
|
||||
|
||||
import os
|
||||
|
||||
# paths
|
||||
CONFIG_FOLDER = ".alice"
|
||||
HOME_DIR = os.path.expanduser("~")
|
||||
APP_DIR = os.path.join(HOME_DIR, CONFIG_FOLDER)
|
||||
THUMBS_PATH = os.path.join(APP_DIR, "images", "thumbnails")
|
||||
TEST_DIR = "/home/cwilvx/Music/Link to Music/Chill"
|
||||
# URL
|
||||
IMG_PATH = os.path.join(APP_DIR, "images")
|
||||
|
||||
THUMBS_PATH = os.path.join(IMG_PATH, "thumbnails")
|
||||
TEST_DIR = "/home/cwilvx/Music/Link to Music/Chill/Wolftyla Radio"
|
||||
# HOME_DIR = TEST_DIR
|
||||
|
||||
# URLS
|
||||
IMG_BASE_URI = "http://127.0.0.1:8900/images/"
|
||||
IMG_ARTIST_URI = IMG_BASE_URI + "artists/"
|
||||
IMG_THUMB_URI = IMG_BASE_URI + "thumbnails/"
|
||||
@@ -23,21 +25,11 @@ DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp"
|
||||
|
||||
LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a"
|
||||
|
||||
P_COLORS = [
|
||||
"rgb(4, 40, 196)",
|
||||
"rgb(196, 4, 68)",
|
||||
"rgb(4, 99, 59)",
|
||||
"rgb(161, 87, 1)",
|
||||
"rgb(1, 161, 22)",
|
||||
"rgb(116, 1, 161)",
|
||||
"rgb(0, 0, 0)",
|
||||
"rgb(95, 95, 95)",
|
||||
"rgb(141, 132, 2)",
|
||||
"rgb(141, 11, 2)",
|
||||
]
|
||||
|
||||
CPU_COUNT = multiprocessing.cpu_count()
|
||||
|
||||
class logger:
|
||||
enable = True
|
||||
THUMB_SIZE: int = 400
|
||||
"""
|
||||
The size of extracted in pixels
|
||||
"""
|
||||
|
||||
LOGGER_ENABLE: bool = True
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 25 KiB |
@@ -8,7 +8,7 @@ gpath=$(poetry run which gunicorn)
|
||||
while getopts ':s' opt; do
|
||||
case $opt in
|
||||
s)
|
||||
echo "🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴"
|
||||
echo "Starting Alice server"
|
||||
cd "./app"
|
||||
"$gpath" -b 0.0.0.0:9877 -w 4 --threads=2 "imgserver:app" &
|
||||
cd ../
|
||||
|
||||
@@ -36,6 +36,7 @@ import useQStore from "@/stores/queue";
|
||||
import useShortcuts from "@/composables/useKeyboard";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { onStartTyping } from "@vueuse/core";
|
||||
|
||||
const context_store = useContextStore();
|
||||
const queue = useQStore();
|
||||
@@ -53,6 +54,10 @@ app_dom.addEventListener("click", (e) => {
|
||||
useRouter().afterEach(() => {
|
||||
document.getElementById("acontent")?.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
onStartTyping(() => {
|
||||
document.getElementById("globalsearch").focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -39,13 +39,18 @@ $teal: rgb(64, 200, 224);
|
||||
|
||||
|
||||
$primary: $gray4;
|
||||
$accent: $darkblue;
|
||||
$accent: $red;
|
||||
$secondary: $gray5;
|
||||
$cta: $blue;
|
||||
$danger: $red;
|
||||
$track-hover: $gray4;
|
||||
$context: $gray5;
|
||||
|
||||
// SVG COLORS
|
||||
$default: $accent;
|
||||
$track-btn-svg: $red;
|
||||
$side-nav-svg: $red;
|
||||
|
||||
// media query mixins
|
||||
@mixin phone-only {
|
||||
@media (max-width: 599px) {
|
||||
|
||||
@@ -72,8 +72,11 @@ a {
|
||||
"l-sidebar content r-sidebar"
|
||||
"l-sidebar content r-sidebar"
|
||||
"l-sidebar content tabs";
|
||||
width: 100vw;
|
||||
width: 100%;
|
||||
align-content: center;
|
||||
max-width: 2720px;
|
||||
height: 100vh;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -87,8 +90,6 @@ a {
|
||||
|
||||
#gsearch-input {
|
||||
grid-area: search-input;
|
||||
// border-left: solid 1px $gray3;
|
||||
// border-bottom: 1px solid $gray3;
|
||||
}
|
||||
|
||||
.topnav {
|
||||
@@ -131,7 +132,7 @@ a {
|
||||
#acontent {
|
||||
grid-area: content;
|
||||
width: 100%;
|
||||
max-width: 1504px;
|
||||
max-width: 1955px;
|
||||
padding: $small;
|
||||
padding-left: 0;
|
||||
overflow: auto;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.9912 22.7422C18.9746 22.7422 23.0879 18.6289 23.0879 13.6543C23.0879 8.67969 18.9658 4.56641 13.9824 4.56641C9.00781 4.56641 4.90332 8.67969 4.90332 13.6543C4.90332 18.6289 9.0166 22.7422 13.9912 22.7422ZM13.9912 20.9316C9.95703 20.9316 6.73145 17.6885 6.73145 13.6543C6.73145 9.62012 9.95703 6.38574 13.9824 6.38574C18.0166 6.38574 21.2598 9.62012 21.2686 13.6543C21.2773 17.6885 18.0254 20.9316 13.9912 20.9316ZM14 17.0996C15.9072 17.0996 17.4453 15.5615 17.4453 13.6455C17.4453 11.7471 15.9072 10.2002 14 10.2002C12.084 10.2002 10.5459 11.7471 10.5459 13.6455C10.5459 15.5615 12.084 17.0996 14 17.0996Z" fill="#F2F2F2"/>
|
||||
<path d="M13.9912 22.7422C18.9746 22.7422 23.0879 18.6289 23.0879 13.6543C23.0879 8.67969 18.9658 4.56641 13.9824 4.56641C9.00781 4.56641 4.90332 8.67969 4.90332 13.6543C4.90332 18.6289 9.0166 22.7422 13.9912 22.7422ZM13.9912 20.9316C9.95703 20.9316 6.73145 17.6885 6.73145 13.6543C6.73145 9.62012 9.95703 6.38574 13.9824 6.38574C18.0166 6.38574 21.2598 9.62012 21.2686 13.6543C21.2773 17.6885 18.0254 20.9316 13.9912 20.9316ZM14 17.0996C15.9072 17.0996 17.4453 15.5615 17.4453 13.6455C17.4453 11.7471 15.9072 10.2002 14 10.2002C12.084 10.2002 10.5459 11.7471 10.5459 13.6455C10.5459 15.5615 12.084 17.0996 14 17.0996Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 737 B |
@@ -0,0 +1,12 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_308_586)">
|
||||
<path d="M23.1318 14.1377C25.5928 14.1377 27.6582 12.0811 27.6582 9.61133C27.6582 7.12402 25.6104 5.08496 23.1318 5.08496C20.6533 5.08496 18.6055 7.13281 18.6055 9.61133C18.6055 12.0986 20.6533 14.1377 23.1318 14.1377ZM6.81055 21.7666H21.3916C23.0879 21.7666 24.0723 20.7822 24.0723 18.9014V15.3066C23.5977 15.4121 22.8857 15.4209 22.3232 15.3066V18.752C22.3232 19.5957 21.875 20.0176 21.0664 20.0176H6.9248C6.11621 20.0176 5.66797 19.5957 5.66797 18.7432V12.0723H17.9551C17.709 11.5977 17.5244 11.0791 17.4365 10.5342H5.66797V8.33691C5.66797 7.53711 6.11621 7.11523 6.89844 7.11523H8.56836C9.19238 7.11523 9.56152 7.26465 10.0625 7.66895L10.5547 8.08203C11.1699 8.57422 11.6445 8.75 12.5674 8.75H17.4453C17.5156 8.12598 17.6562 7.58105 17.9551 6.99219H13.0332C12.4004 6.99219 12.0225 6.85156 11.5303 6.44727L11.0381 6.04297C10.4141 5.5332 9.95703 5.36621 9.03418 5.36621H6.54688C4.89453 5.36621 3.91895 6.33301 3.91895 8.1875V18.9014C3.91895 20.791 4.91211 21.7666 6.81055 21.7666ZM20.8643 10.2178C20.5391 10.2178 20.2578 9.92773 20.2578 9.61133C20.2578 9.28613 20.5391 9.00488 20.8643 9.00488H25.3994C25.7334 9.00488 26.0059 9.28613 26.0059 9.61133C26.0059 9.92773 25.7334 10.2178 25.3994 10.2178H20.8643Z" fill="#fff"/>
|
||||
<path d="M26.7314 6.32392C28.484 8.0765 28.4777 10.9912 26.7376 12.7314C24.9788 14.4902 22.0827 14.4902 20.3239 12.7314C18.5713 10.9788 18.5713 8.08271 20.3301 6.32392C22.0827 4.57135 24.9788 4.57135 26.7314 6.32392Z" fill="#fff"/>
|
||||
<path d="M21.4986 10.6246C21.2438 10.8794 21.2438 11.2896 21.5048 11.5506C21.7721 11.8178 22.1885 11.8241 22.4433 11.5693L23.9783 10.0342L24.662 9.27599V9.95962L24.6247 11.0783C24.6185 11.2461 24.6806 11.4201 24.8111 11.5382C25.0535 11.7806 25.4326 11.7992 25.6688 11.5506C25.793 11.4139 25.849 11.2585 25.8366 11.0721L25.7371 8.01438C25.7309 7.77201 25.6812 7.62285 25.5569 7.49856C25.4388 7.38047 25.2834 7.33697 25.0535 7.33076L21.9834 7.21889C21.8031 7.21268 21.6478 7.26861 21.5173 7.39912C21.2749 7.6415 21.2749 8.02681 21.5297 8.25676C21.6478 8.37484 21.8218 8.43699 21.9834 8.43699L23.1083 8.40592L23.7919 8.39349L23.0337 9.08955L21.4986 10.6246Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_308_586">
|
||||
<rect width="28" height="28" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -8,10 +8,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['bio'],
|
||||
};
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
bio: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -48,7 +48,6 @@ export default {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
|
||||
.rect {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
@@ -59,10 +58,10 @@ export default {
|
||||
left: 7rem;
|
||||
transform: rotate(45deg) translate(-1rem, -9rem);
|
||||
z-index: 1;
|
||||
transition: all .5s ease-in-out;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transition: all .5s ease-in-out;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,4 +85,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<template>
|
||||
<div class="album-h" ref="albumheaderthing">
|
||||
<div class="a-header rounded">
|
||||
<div
|
||||
class="a-header rounded"
|
||||
:style="{
|
||||
backgroundImage: `linear-gradient(
|
||||
37deg, ${props.album.colors[0]}, ${props.album.colors[3]}
|
||||
)`,
|
||||
}"
|
||||
>
|
||||
<div class="art">
|
||||
<div
|
||||
class="image shadow-lg rounded"
|
||||
:style="{
|
||||
backgroundImage: `url("${imguri + album.image}")`,
|
||||
}"
|
||||
<img
|
||||
:src="imguri + album.image"
|
||||
alt=""
|
||||
v-motion-slide-from-left
|
||||
></div>
|
||||
class="rounded shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="info" :class="{ nocontrast: isLight() }">
|
||||
<div class="top" v-motion-slide-from-top>
|
||||
<div class="h">
|
||||
<span v-if="album.is_soundtrack">Soundtrack</span>
|
||||
@@ -18,7 +24,9 @@
|
||||
<span v-else-if="album.is_single">Single</span>
|
||||
<span v-else>Album</span>
|
||||
</div>
|
||||
<div class="title ellip">{{ album.title }}</div>
|
||||
<div class="title ellip">
|
||||
{{ album.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="stats">
|
||||
@@ -26,7 +34,11 @@
|
||||
{{ formatSeconds(album.duration, true) }} • {{ album.date }} •
|
||||
{{ album.artist }}
|
||||
</div>
|
||||
<PlayBtnRect :source="playSources.album" :store="useAlbumStore" />
|
||||
<PlayBtnRect
|
||||
:source="playSources.album"
|
||||
:store="useAlbumStore"
|
||||
:background="getButtonColor()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,31 +56,133 @@ import { paths } from "../../config";
|
||||
import { AlbumInfo } from "../../interfaces";
|
||||
import PlayBtnRect from "../shared/PlayBtnRect.vue";
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
album: AlbumInfo;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "resetBottomPadding"): void;
|
||||
}>();
|
||||
|
||||
const albumheaderthing = ref<HTMLElement>(null);
|
||||
const imguri = paths.images.thumb;
|
||||
const nav = useNavStore();
|
||||
|
||||
useVisibility(albumheaderthing, nav.toggleShowPlay);
|
||||
/**
|
||||
* Calls the `toggleShowPlay` method which toggles the play button in the nav.
|
||||
* Emits the `resetBottomPadding` event to reset the album page content bottom padding.
|
||||
*
|
||||
* @param {boolean} state the new visibility state of the album page header.
|
||||
*/
|
||||
function handleVisibilityState(state: boolean) {
|
||||
if (state) {
|
||||
emit("resetBottomPadding");
|
||||
}
|
||||
|
||||
nav.toggleShowPlay(state);
|
||||
}
|
||||
|
||||
useVisibility(albumheaderthing, handleVisibilityState);
|
||||
|
||||
/**
|
||||
* Returns `true` if the rgb color passed is light.
|
||||
*
|
||||
* @param {string} rgb The color to check whether it's light or dark.
|
||||
* @returns {boolean} true if color is light, false if color is dark.
|
||||
*/
|
||||
function isLight(rgb: string = props.album.colors[0]): boolean {
|
||||
if (rgb == null || undefined) return false;
|
||||
|
||||
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
|
||||
return brightness > 170;
|
||||
}
|
||||
|
||||
interface BtnColor {
|
||||
color: string;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first contrasting color in the album colors.
|
||||
*
|
||||
* @param {string[]} colors The album colors to choose from.
|
||||
* @returns {BtnColor} A color to use as the play button background
|
||||
*/
|
||||
function getButtonColor(colors: string[] = props.album.colors): BtnColor {
|
||||
const base_color = colors[0];
|
||||
if (colors.length === 0) return { color: "#fff", isDark: true };
|
||||
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
if (theyContrast(base_color, colors[i])) {
|
||||
return {
|
||||
color: colors[i],
|
||||
isDark: isLight(colors[i]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
color: "#fff",
|
||||
isDark: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the luminance of a color.
|
||||
* @param r The red value of the color.
|
||||
* @param g The green value of the color.
|
||||
* @param b The blue value of the color.
|
||||
*/
|
||||
function luminance(r: any, g: any, b: any) {
|
||||
let a = [r, g, b].map(function (v) {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a contrast ratio of `color1`:`color2`
|
||||
* @param {string} color1 The first color
|
||||
* @param {string} color2 The second color
|
||||
*/
|
||||
function contrast(color1: number[], color2: number[]): number {
|
||||
let lum1 = luminance(color1[0], color1[1], color1[2]);
|
||||
let lum2 = luminance(color2[0], color2[1], color2[2]);
|
||||
let brightest = Math.max(lum1, lum2);
|
||||
let darkest = Math.min(lum1, lum2);
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a rgb color string to an array of the form: `[r, g, b]`
|
||||
* @param rgb The color to convert
|
||||
* @returns {number[]} The array representation of the color
|
||||
*/
|
||||
function rgbToArray(rgb: string): number[] {
|
||||
return rgb.match(/\d+/g)!.map(Number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the `color2` contrast with `color1`.
|
||||
* @param color1 The first color
|
||||
* @param color2 The second color
|
||||
*/
|
||||
function theyContrast(color1: string, color2: string) {
|
||||
return contrast(rgbToArray(color1), rgbToArray(color2)) > 3;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.album-h {
|
||||
height: 16rem;
|
||||
}
|
||||
|
||||
.a-header {
|
||||
display: grid;
|
||||
grid-template-columns: 15rem 1fr;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
background-color: $black;
|
||||
background-color: #000000;
|
||||
background-image: linear-gradient(37deg, $black 20%, $gray, $black 90%);
|
||||
|
||||
.art {
|
||||
@@ -78,12 +192,17 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.image {
|
||||
width: 14rem;
|
||||
height: 14rem;
|
||||
img {
|
||||
width: 15rem;
|
||||
height: 15rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.nocontrast {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -91,20 +210,14 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
|
||||
justify-content: flex-end;
|
||||
|
||||
.top {
|
||||
.h {
|
||||
color: #ffffffcb;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.artist {
|
||||
font-size: 1.15rem;
|
||||
color: #ffffffe0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,23 +234,6 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.play {
|
||||
height: 2.5rem;
|
||||
width: 6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $blue;
|
||||
padding: $small;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
margin-right: $small;
|
||||
background: url(../../assets/icons/play.svg) no-repeat center/cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
<template>
|
||||
<router-link
|
||||
:to="{ name: 'FolderView', params: { path: props.folder.path } }"
|
||||
>
|
||||
<router-link :to="{ name: 'FolderView', params: { path: folder.path } }">
|
||||
<div class="f-item">
|
||||
<div class="icon image"></div>
|
||||
<div class="icon">
|
||||
<FolderSvg v-if="!folder.is_sym" />
|
||||
<SymLinkSvg v-if="folder.is_sym" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="f-item-text ellip">{{ props.folder.name }}</div>
|
||||
<div class="f-item-text ellip">{{ folder.name }}</div>
|
||||
<div class="separator no-border"></div>
|
||||
<div class="f-item-count">{{ props.folder.trackcount }} tracks</div>
|
||||
<div class="f-item-count">{{ folder.trackcount }} tracks</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
folder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
import { Folder } from "@/interfaces";
|
||||
import FolderSvg from "../../assets/icons/folder.svg";
|
||||
import SymLinkSvg from "../../assets/icons/symlink.svg";
|
||||
|
||||
defineProps<{
|
||||
folder: Folder;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.f-container .f-item {
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
padding-right: 1rem;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background-color: $gray4;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0.75rem;
|
||||
@@ -37,16 +40,10 @@ const props = defineProps({
|
||||
}
|
||||
|
||||
.icon {
|
||||
background-image: url(../../assets/icons/folder.svg);
|
||||
width: 2rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
margin: 0 0.75rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
|
||||
.f-item-count {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(219, 217, 217, 0.63);
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
<template>
|
||||
<div class="folder">
|
||||
<div class="table rounded" v-if="tracks.length">
|
||||
<div class="thead">
|
||||
<div class="index"></div>
|
||||
<div class="track-header">Track</div>
|
||||
<div class="artists-header">Artist</div>
|
||||
<div class="album-header">Album</div>
|
||||
<div class="duration-header">Duration</div>
|
||||
</div>
|
||||
<div class="songlist">
|
||||
<SongItem
|
||||
v-for="(track, index) in tracks"
|
||||
v-for="track in getTracks()"
|
||||
:key="track.trackid"
|
||||
:song="track"
|
||||
:index="index + 1"
|
||||
:index="track.index"
|
||||
@updateQueue="updateQueue"
|
||||
:isPlaying="queue.playing"
|
||||
:isCurrent="queue.current.trackid == track.trackid"
|
||||
:isCurrent="queue.currentid == track.trackid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,6 +36,7 @@ const props = defineProps<{
|
||||
path?: string;
|
||||
pname?: string;
|
||||
playlistid?: string;
|
||||
on_album_page?: boolean;
|
||||
}>();
|
||||
|
||||
let route = useRoute().name;
|
||||
@@ -53,21 +47,43 @@ let route = useRoute().name;
|
||||
* @param track Track object
|
||||
*/
|
||||
function updateQueue(track: Track) {
|
||||
const index = props.tracks.findIndex(
|
||||
(t: Track) => t.trackid === track.trackid
|
||||
);
|
||||
|
||||
switch (route) {
|
||||
case "FolderView":
|
||||
queue.playFromFolder(props.path, props.tracks);
|
||||
queue.play(track);
|
||||
queue.play(index);
|
||||
break;
|
||||
case "AlbumView":
|
||||
queue.playFromAlbum(track.album, track.albumartist, props.tracks);
|
||||
queue.play(track);
|
||||
queue.play(index);
|
||||
break;
|
||||
case "PlaylistView":
|
||||
queue.playFromPlaylist(props.pname, props.playlistid, props.tracks);
|
||||
queue.play(track);
|
||||
queue.play(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getTracks() {
|
||||
if (props.on_album_page) {
|
||||
let tracks = props.tracks.map((track, index) => {
|
||||
track.index = track.tracknumber;
|
||||
return track;
|
||||
});
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
let tracks = props.tracks.map((track, index) => {
|
||||
track.index = index + 1;
|
||||
return track;
|
||||
});
|
||||
|
||||
return tracks;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -97,47 +113,6 @@ function updateQueue(track: Track) {
|
||||
}
|
||||
}
|
||||
|
||||
.thead {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 0.25fr;
|
||||
height: 2.5rem;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: $gray1;
|
||||
gap: $small;
|
||||
|
||||
@include tablet-landscape {
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr;
|
||||
}
|
||||
|
||||
@include tablet-portrait {
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr;
|
||||
}
|
||||
|
||||
@include phone-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.duration-header {
|
||||
@include tablet-landscape {
|
||||
display: none;
|
||||
}
|
||||
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.album-header {
|
||||
@include tablet-portrait {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.songlist {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<div class="nav-button" id="home-button" v-motion-slide-from-left-100>
|
||||
<div class="in">
|
||||
<div class="nav-icon image" :id="`${menu.name}-icon`"></div>
|
||||
<component :is="menu.icon"></component>
|
||||
<span>{{ menu.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,39 +16,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import PlaylistSvg from "../../assets/icons/playlist.svg";
|
||||
import FolderSvg from "../../assets/icons/folder.svg";
|
||||
|
||||
const menus = [
|
||||
{
|
||||
name: "home",
|
||||
route_name: "Home",
|
||||
},
|
||||
{
|
||||
name: "albums",
|
||||
route_name: "AlbumsView",
|
||||
},
|
||||
{
|
||||
name: "artists",
|
||||
route_name: "ArtistsView",
|
||||
},
|
||||
{
|
||||
name: "playlists",
|
||||
route_name: "Playlists",
|
||||
icon: PlaylistSvg,
|
||||
},
|
||||
{
|
||||
name: "folders",
|
||||
route_name: "FolderView",
|
||||
params: { path: "$home" },
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
},
|
||||
{
|
||||
name: "settings",
|
||||
route_name: "SettingsView",
|
||||
icon: FolderSvg,
|
||||
},
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -84,36 +67,17 @@ const menus = [
|
||||
background-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#home-icon {
|
||||
background-image: url(../../assets/icons/home.svg);
|
||||
}
|
||||
svg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin: 0 $small 0 $small;
|
||||
border-radius: $small;
|
||||
}
|
||||
|
||||
#albums-icon {
|
||||
background-image: url(../../assets/icons/album.svg);
|
||||
}
|
||||
|
||||
#artists-icon {
|
||||
background-image: url(../../assets/icons/artist.svg);
|
||||
}
|
||||
|
||||
#playlists-icon {
|
||||
background-image: url(../../assets/icons/playlist.svg);
|
||||
}
|
||||
|
||||
#mixes-icon {
|
||||
background-image: url(../../assets/icons/mix.svg);
|
||||
}
|
||||
|
||||
#folders-icon {
|
||||
background-image: url(../../assets/icons/folder.svg);
|
||||
}
|
||||
#settings-icon {
|
||||
background-image: url(../../assets/icons/settings.svg);
|
||||
}
|
||||
#tags-icon {
|
||||
background-image: url(../../assets/icons/tag.svg);
|
||||
}
|
||||
svg > path {
|
||||
fill: $accent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
<div class="info">
|
||||
<div class="desc">
|
||||
<div>
|
||||
<div class="art">
|
||||
<div
|
||||
class="l-image image rounded"
|
||||
:style="{
|
||||
backgroundImage: `url("${imguri + track.image}")`,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'AlbumView',
|
||||
params: {
|
||||
hash: track.albumhash,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div class="art">
|
||||
<img :src="imguri + track.image" alt="" class="l-image rounded" />
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<div id="bitrate">
|
||||
<span v-if="track.bitrate > 1500">MASTER</span>
|
||||
<span v-else-if="track.bitrate > 330">FLAC</span>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<template>
|
||||
<div class="l_ rounded">
|
||||
<div class="headin">Now Playing</div>
|
||||
<div class="button menu image rounded"></div>
|
||||
<div
|
||||
class="button menu rounded"
|
||||
@click="showContextMenu"
|
||||
:class="{ context_on: context_on }"
|
||||
>
|
||||
<MenuSvg />
|
||||
</div>
|
||||
<div class="separator no-border"></div>
|
||||
<div>
|
||||
<SongCard :track="queue.current" />
|
||||
<SongCard :track="queue.tracks[queue.current]" />
|
||||
<Progress />
|
||||
<HotKeys />
|
||||
</div>
|
||||
@@ -16,8 +22,38 @@ import SongCard from "./SongCard.vue";
|
||||
import HotKeys from "./NP/HotKeys.vue";
|
||||
import Progress from "./NP/Progress.vue";
|
||||
import useQStore from "../../stores/queue";
|
||||
import MenuSvg from "../../assets/icons/more.svg";
|
||||
import trackContext from "@/contexts/track_context";
|
||||
import useContextStore from "@/stores/context";
|
||||
import useModalStore from "@/stores/modal";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import { ContextSrc } from "@/composables/enums";
|
||||
|
||||
import { ref } from "vue";
|
||||
|
||||
const queue = useQStore();
|
||||
const contextStore = useContextStore();
|
||||
const context_on = ref(false);
|
||||
|
||||
const showContextMenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const menus = trackContext(
|
||||
queue.tracks[queue.current],
|
||||
useModalStore,
|
||||
useQueueStore
|
||||
);
|
||||
|
||||
contextStore.showContextMenu(e, menus, ContextSrc.Track);
|
||||
context_on.value = true;
|
||||
|
||||
contextStore.$subscribe((mutation, state) => {
|
||||
if (!state.visible) {
|
||||
context_on.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.l_ {
|
||||
@@ -49,31 +85,26 @@ const queue = useQStore();
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
position: absolute;
|
||||
background-size: 1.5rem;
|
||||
top: $small;
|
||||
cursor: pointer;
|
||||
transition: all 200ms;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $smaller;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray2;
|
||||
background-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
.context_on {
|
||||
background-color: $accent;
|
||||
}
|
||||
|
||||
.menu {
|
||||
right: $small;
|
||||
background-image: url("../../assets/icons/right-arrow.svg");
|
||||
transform: rotate(90deg);
|
||||
|
||||
&:hover {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
height: 0rem;
|
||||
}
|
||||
|
||||
.art {
|
||||
@@ -102,6 +133,7 @@ const queue = useQStore();
|
||||
|
||||
.title {
|
||||
font-weight: 900;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.artists {
|
||||
|
||||
@@ -3,81 +3,66 @@
|
||||
<div class="header">
|
||||
<div class="headin">Featured Artists</div>
|
||||
<div class="xcontrols">
|
||||
<div class="prev" @click="scrollLeft"></div>
|
||||
<div class="next" @click="scrollRight"></div>
|
||||
<div class="prev icon" @click="scrollLeft()"><ArrowSvg /></div>
|
||||
<div class="next icon" @click="scrollRight()"><ArrowSvg /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator no-border"></div>
|
||||
<div class="artists" ref="artists_dom">
|
||||
<ArtistCard
|
||||
v-for="artist in artists"
|
||||
:key="artist"
|
||||
:key="artist.image"
|
||||
:artist="artist"
|
||||
:color="'ffffff00'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "@vue/reactivity";
|
||||
import ArtistCard from "@/components/shared/ArtistCard.vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import { Artist } from "@/interfaces";
|
||||
import ArrowSvg from "../../assets/icons/right-arrow.svg";
|
||||
|
||||
export default {
|
||||
props: ["artists"],
|
||||
components: {
|
||||
ArtistCard,
|
||||
},
|
||||
setup() {
|
||||
const artists_dom = ref(null);
|
||||
defineProps<{
|
||||
artists: Artist[];
|
||||
}>();
|
||||
|
||||
const scrollLeft = () => {
|
||||
const dom = artists_dom.value;
|
||||
dom.scrollBy({
|
||||
left: -700,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
const artists_dom = ref(null);
|
||||
|
||||
const scrollRight = () => {
|
||||
const dom = artists_dom.value;
|
||||
dom.scrollBy({
|
||||
left: 700,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
const scrollLeft = () => {
|
||||
const dom = artists_dom.value;
|
||||
dom.scrollBy({
|
||||
left: -700,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
artists_dom,
|
||||
scrollLeft,
|
||||
scrollRight,
|
||||
};
|
||||
},
|
||||
const scrollRight = () => {
|
||||
const dom = artists_dom.value;
|
||||
dom.scrollBy({
|
||||
left: 700,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.f-artists {
|
||||
height: 14.5em;
|
||||
width: calc(100%);
|
||||
padding: $small;
|
||||
padding-bottom: 0;
|
||||
width: 100%;
|
||||
padding: 0 $small;
|
||||
border-radius: $small;
|
||||
user-select: none;
|
||||
background: linear-gradient(0deg, transparent, $black);
|
||||
position: relative;
|
||||
background-color: #ffffff00;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.headin {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 900;
|
||||
// border: solid;
|
||||
margin-left: $small;
|
||||
}
|
||||
}
|
||||
@@ -85,40 +70,31 @@ export default {
|
||||
|
||||
.f-artists .xcontrols {
|
||||
z-index: 1;
|
||||
width: 5rem;
|
||||
height: 2rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.next {
|
||||
background: url(../../assets/icons/right-arrow.svg) no-repeat center;
|
||||
}
|
||||
gap: 1rem;
|
||||
|
||||
.prev {
|
||||
background: url(../../assets/icons/right-arrow.svg) no-repeat center;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.next,
|
||||
.prev {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
|
||||
.icon {
|
||||
border-radius: $small;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease;
|
||||
background-color: rgb(51, 51, 51);
|
||||
}
|
||||
padding: $smaller;
|
||||
|
||||
.next:hover,
|
||||
.prev:hover {
|
||||
background-color: $blue;
|
||||
transition: all 0.5s ease;
|
||||
svg {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $accent;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,14 @@
|
||||
<div class="carddd">
|
||||
<div class="info">
|
||||
<div class="btns">
|
||||
<PlayBtnRect :source="playSources.playlist" :store="usePStore" />
|
||||
<PlayBtnRect
|
||||
:source="playSources.playlist"
|
||||
:store="usePStore"
|
||||
:background="{
|
||||
color: '#fff',
|
||||
isDark: true,
|
||||
}"
|
||||
/>
|
||||
<Option @showDropdown="showDropdown" :src="context.src" />
|
||||
</div>
|
||||
<div class="duration">
|
||||
@@ -20,7 +27,8 @@
|
||||
<span v-else-if="props.info.count == 1"
|
||||
>{{ props.info.count }} Track</span
|
||||
>
|
||||
<span v-else>{{ props.info.count }} Tracks</span> • 3 Hours
|
||||
<span v-else>{{ props.info.count }} Tracks</span> •
|
||||
{{ formatSeconds(props.info.duration, true) }}
|
||||
</div>
|
||||
<div class="desc">
|
||||
{{ props.info.description }}
|
||||
@@ -51,6 +59,7 @@ import useContextStore from "../../stores/context";
|
||||
import useModalStore from "../../stores/modal";
|
||||
import Option from "../shared/Option.vue";
|
||||
import PlayBtnRect from "../shared/PlayBtnRect.vue";
|
||||
import { formatSeconds } from "@/composables/perks";
|
||||
|
||||
const imguri = paths.images.playlist;
|
||||
const context = useContextStore();
|
||||
@@ -77,7 +86,7 @@ function showDropdown(e: any) {
|
||||
.p-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
height: 16rem;
|
||||
height: 17rem;
|
||||
position: relative;
|
||||
border-radius: 0.75rem;
|
||||
color: $white;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="r-home">
|
||||
<UpNext :next="queue.next" :playNext="queue.playNext" />
|
||||
<UpNext :next="queue.tracks[queue.next]" :playNext="queue.playNext" />
|
||||
<Recommendations />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
<div class="r-search" v-show="tabs.current === tabs.tabs.search">
|
||||
<Search />
|
||||
</div>
|
||||
|
||||
<div class="r-queue" v-show="tabs.current === tabs.tabs.queue">
|
||||
<UpNext />
|
||||
<Queue />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +18,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Search from "./Search/Main.vue";
|
||||
import UpNext from "./Queue.vue";
|
||||
import Queue from "./Queue.vue";
|
||||
import DashBoard from "./Home/Main.vue";
|
||||
import useTabStore from "../../stores/tabs";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="up-next">
|
||||
<div class="r-grid">
|
||||
<UpNext :next="queue.next" :playNext="queue.playNext" />
|
||||
<UpNext :next="queue.tracks[queue.next]" :playNext="queue.playNext" />
|
||||
<div class="scrollable-r border rounded">
|
||||
<div
|
||||
class="inner"
|
||||
@@ -9,11 +9,11 @@
|
||||
@mouseleave="setMouseOver(false)"
|
||||
>
|
||||
<TrackItem
|
||||
v-for="t in queue.tracks"
|
||||
v-for="(t, index) in queue.tracks"
|
||||
:key="t.trackid"
|
||||
:track="t"
|
||||
@playThis="queue.play(t)"
|
||||
:isCurrent="t.trackid === queue.current.trackid"
|
||||
@playThis="queue.play(index)"
|
||||
:isCurrent="index === queue.current"
|
||||
:isPlaying="queue.playing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ import useSearchStore from "../../../stores/search";
|
||||
const search = useSearchStore();
|
||||
|
||||
function loadMore() {
|
||||
search.updateLoadCounter("albums", 6);
|
||||
search.updateLoadCounter("albums");
|
||||
search.loadAlbums(search.loadCounter.albums);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,7 @@ import useSearchStore from "../../../stores/search";
|
||||
const search = useSearchStore();
|
||||
|
||||
function loadMore() {
|
||||
search.updateLoadCounter("artists", 6);
|
||||
search.updateLoadCounter("artists");
|
||||
search.loadArtists(search.loadCounter.artists);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="albums-results border">
|
||||
<div class="grid">
|
||||
<PCard
|
||||
v-for="album in search.albums.value"
|
||||
:key="`${album.artist}-${album.title}`"
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
<LoadMore v-if="search.albums.more" @loadMore="loadMore()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PCard from "../../playlists/PlaylistCard.vue";
|
||||
import LoadMore from "./LoadMore.vue";
|
||||
import useSearchStore from "../../../stores/search";
|
||||
|
||||
const search = useSearchStore();
|
||||
|
||||
function loadMore() {
|
||||
search.updateLoadCounter("albums");
|
||||
search.loadAlbums(search.loadCounter.albums);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.right-search .albums-results {
|
||||
border-radius: 0.5rem;
|
||||
margin-top: $small;
|
||||
padding: $small;
|
||||
overflow-x: hidden;
|
||||
|
||||
.result-item:hover {
|
||||
background-color: $gray4;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,13 +2,13 @@
|
||||
<div id="tracks-results" v-if="search.tracks.value">
|
||||
<TransitionGroup name="list">
|
||||
<TrackItem
|
||||
v-for="track in search.tracks.value"
|
||||
v-for="(track, index) in search.tracks.value"
|
||||
:key="track.trackid"
|
||||
:track="track"
|
||||
:isPlaying="queue.playing"
|
||||
:isCurrent="queue.current.trackid == track.trackid"
|
||||
:isCurrent="queue.currentid == track.trackid"
|
||||
:isSearchTrack="true"
|
||||
@PlayThis="updateQueue"
|
||||
@PlayThis="updateQueue(index)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<LoadMore v-if="search.tracks.more" @loadMore="loadMore" />
|
||||
@@ -26,13 +26,13 @@ const queue = useQStore();
|
||||
const search = useSearchStore();
|
||||
|
||||
function loadMore() {
|
||||
search.updateLoadCounter("tracks", 5);
|
||||
search.updateLoadCounter("tracks");
|
||||
search.loadTracks(search.loadCounter.tracks);
|
||||
}
|
||||
|
||||
function updateQueue(track: Track) {
|
||||
function updateQueue(index: number) {
|
||||
queue.playFromSearch(search.query, search.tracks.value);
|
||||
queue.play(track);
|
||||
queue.play(index);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div id="ginner" tabindex="0">
|
||||
<div class="icon image"></div>
|
||||
<input
|
||||
id="search"
|
||||
id="globalsearch"
|
||||
class="rounded"
|
||||
v-model="search.query"
|
||||
placeholder="Search your library"
|
||||
@@ -16,6 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import useSearchStore from "../../stores/search";
|
||||
|
||||
const search = useSearchStore();
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
v-for="option in context.options"
|
||||
:key="option.label"
|
||||
:class="[{ critical: option.critical }, option.type]"
|
||||
@click="option.action"
|
||||
@click="option.action()"
|
||||
>
|
||||
<div class="icon image" :class="option.icon"></div>
|
||||
<div class="label ellip">{{ option.label }}</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div
|
||||
class="context-item"
|
||||
v-for="child in option.children"
|
||||
:key="child"
|
||||
:key="child.label"
|
||||
:class="[{ critical: child.critical }, child.type]"
|
||||
@click="child.action()"
|
||||
>
|
||||
@@ -57,7 +57,7 @@ const context = useContextStore();
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 12rem;
|
||||
z-index: 10;
|
||||
z-index: 10000 !important;
|
||||
transform: scale(0);
|
||||
|
||||
padding: $small;
|
||||
@@ -68,12 +68,10 @@ const context = useContextStore();
|
||||
.context-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
padding: $small;
|
||||
border-radius: $small;
|
||||
color: rgb(255, 255, 255);
|
||||
position: relative;
|
||||
text-transform: capitalize;
|
||||
|
||||
@@ -141,7 +139,7 @@ const context = useContextStore();
|
||||
|
||||
.children {
|
||||
transform: scale(1);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
transition: transform 0.1s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +158,6 @@ const context = useContextStore();
|
||||
|
||||
.context-menu-visible {
|
||||
transform: scale(1);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.context-normalizedX {
|
||||
@@ -177,7 +174,7 @@ const context = useContextStore();
|
||||
.context-normalizedY {
|
||||
.context-item > .children {
|
||||
transform-origin: bottom right;
|
||||
top: -.5rem;
|
||||
top: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { createNewPlaylist } from "../../composables/playlists";
|
||||
import { createNewPlaylist } from "../../composables/pages/playlists";
|
||||
import { Track } from "../../interfaces";
|
||||
import { Notification, NotifType } from "../../stores/notification";
|
||||
import usePlaylistStore from "@/stores/pages/playlists";
|
||||
@@ -43,22 +43,22 @@ emit("title", "New Playlist");
|
||||
/**
|
||||
* Create a new playlist. If this modal is called with a track,
|
||||
* add the track to the new playlist.
|
||||
* @param e Event
|
||||
* @param {Event} e
|
||||
*/
|
||||
function create(e: Event) {
|
||||
e.preventDefault();
|
||||
const name = (e.target as HTMLFormElement).elements["name"].value;
|
||||
|
||||
if (name.trim()) {
|
||||
createNewPlaylist(name, props.track).then((status) => {
|
||||
createNewPlaylist(name, props.track).then(({ success, playlist }) => {
|
||||
emit("hideModal");
|
||||
|
||||
if (!status.success) return;
|
||||
if (!success) return;
|
||||
|
||||
if (route.name !== "Playlists") return;
|
||||
|
||||
setTimeout(() => {
|
||||
playlistStore.addPlaylist(status.playlist);
|
||||
playlistStore.addPlaylist(playlist);
|
||||
}, 600);
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -41,7 +41,13 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="submit">
|
||||
<input type="submit" id="updateplaylistsubmit" class="rounded" value="Update" @click="" />
|
||||
<input
|
||||
type="submit"
|
||||
id="updateplaylistsubmit"
|
||||
class="rounded"
|
||||
value="Update"
|
||||
@click=""
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -49,7 +55,7 @@
|
||||
<script setup lang="ts">
|
||||
import usePStore from "@/stores/pages/playlist";
|
||||
import { onMounted } from "vue";
|
||||
import { updatePlaylist } from "../../composables/playlists";
|
||||
import { updatePlaylist } from "../../composables/pages/playlists";
|
||||
import { Playlist } from "../../interfaces";
|
||||
|
||||
const pStore = usePStore();
|
||||
@@ -105,8 +111,10 @@ function update_playlist(e: Event) {
|
||||
|
||||
if (!clicked) {
|
||||
clicked = true;
|
||||
const elem = document.getElementById("updateplaylistsubmit") as HTMLFormElement
|
||||
elem.value = "Updating"
|
||||
const elem = document.getElementById(
|
||||
"updateplaylistsubmit"
|
||||
) as HTMLFormElement;
|
||||
elem.value = "Updating";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
:playlist="props.playlist"
|
||||
class="p-card rounded"
|
||||
>
|
||||
<div class="drop">
|
||||
<Option :color="'#48484a'" />
|
||||
</div>
|
||||
<div
|
||||
class="image p-image rounded shadow-sm"
|
||||
:style="{
|
||||
@@ -28,12 +25,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Playlist } from "../../interfaces";
|
||||
import PlayBtn from "../shared/PlayBtn.vue";
|
||||
import Option from "../shared/Option.vue";
|
||||
import { paths } from "../../config";
|
||||
|
||||
const imguri = paths.images.playlist
|
||||
|
||||
const imguri = paths.images.playlist;
|
||||
|
||||
const props = defineProps<{
|
||||
playlist: Playlist;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'AlbumView',
|
||||
params: { album: album.title, artist: album.artist },
|
||||
params: { hash: album.hash },
|
||||
}"
|
||||
class="result-item"
|
||||
>
|
||||
|
||||
@@ -38,11 +38,11 @@ defineProps<{
|
||||
cursor: pointer;
|
||||
|
||||
.artist-image {
|
||||
width: 7em;
|
||||
height: 7em;
|
||||
width: 8em;
|
||||
height: 8em;
|
||||
border-radius: 60%;
|
||||
margin-bottom: $small;
|
||||
background-size: 7rem 7rem;
|
||||
background-size: 8rem 8rem;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
transition: all 0.5s ease-in-out;
|
||||
@@ -63,6 +63,7 @@ defineProps<{
|
||||
font-size: 0.9rem;
|
||||
font-weight: 510;
|
||||
max-width: 7rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
<div
|
||||
class="playbtnrect rounded"
|
||||
@click="usePlayFrom(source, useQStore, store)"
|
||||
:style="{
|
||||
backgroundColor: background.color,
|
||||
}"
|
||||
:class="{ playbtnrectdark: background.isDark }"
|
||||
>
|
||||
<div class="icon image"></div>
|
||||
<playBtnSvg />
|
||||
<div class="text">Play</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,13 +15,18 @@
|
||||
<script setup lang="ts">
|
||||
import { playSources } from "@/composables/enums";
|
||||
import usePlayFrom from "@/composables/usePlayFrom";
|
||||
import useFStore from "@/stores/folder";
|
||||
import useFStore from "@/stores/pages/folder";
|
||||
import useAStore from "@/stores/pages/album";
|
||||
import usePStore from "@/stores/pages/playlist";
|
||||
import useQStore from "@/stores/queue";
|
||||
import playBtnSvg from "@/assets/icons/play.svg";
|
||||
|
||||
defineProps<{
|
||||
source: playSources;
|
||||
background?: {
|
||||
color: string;
|
||||
isDark?: boolean;
|
||||
};
|
||||
store:
|
||||
| typeof useQStore
|
||||
| typeof useFStore
|
||||
@@ -34,8 +43,8 @@ defineProps<{
|
||||
height: 2.5rem;
|
||||
padding-left: 0.75rem;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(34deg, $accent, $red);
|
||||
user-select: none;
|
||||
color: $white;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
.icon {
|
||||
@@ -50,4 +59,12 @@ defineProps<{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playbtnrectdark {
|
||||
color: $black !important;
|
||||
|
||||
svg > path {
|
||||
fill: $accent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
>
|
||||
<div class="index">{{ props.index }}</div>
|
||||
<div class="flex">
|
||||
<div
|
||||
class="album-art image rounded"
|
||||
:style="{
|
||||
backgroundImage: `url("${imguri + props.song.image}"`,
|
||||
}"
|
||||
@click="emitUpdate(props.song)"
|
||||
>
|
||||
<div @click="emitUpdate(props.song)" class="thumbnail">
|
||||
<img
|
||||
:src="imguri + props.song.image"
|
||||
alt=""
|
||||
class="album-art image rounded"
|
||||
/>
|
||||
<div
|
||||
class="now-playing-track image"
|
||||
v-if="props.isPlaying && props.isCurrent"
|
||||
@@ -38,41 +37,52 @@
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
class="song-album"
|
||||
class="song-album ellip"
|
||||
:to="{
|
||||
name: 'AlbumView',
|
||||
params: {
|
||||
album: props.song.album,
|
||||
artist: props.song.albumartist,
|
||||
hash: props.song.albumhash,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div class="album ellip">
|
||||
{{ props.song.album }}
|
||||
</div>
|
||||
{{ props.song.album }}
|
||||
</router-link>
|
||||
<div class="song-duration">
|
||||
{{ formatSeconds(props.song.length) }}
|
||||
<div class="text">{{ formatSeconds(props.song.length) }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="options-icon circular"
|
||||
:class="{ options_button_clicked }"
|
||||
@click="
|
||||
(e) => {
|
||||
showContextMenu(e);
|
||||
options_button_clicked = true;
|
||||
}
|
||||
"
|
||||
>
|
||||
<OptionSvg />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { putCommas, formatSeconds } from "../../composables/perks";
|
||||
import useContextStore from "../../stores/context";
|
||||
import useModalStore from "../../stores/modal";
|
||||
import useQueueStore from "../../stores/queue";
|
||||
import { ContextSrc } from "../../composables/enums";
|
||||
import { putCommas, formatSeconds } from "@/composables/perks";
|
||||
import useContextStore from "@/stores/context";
|
||||
import useModalStore from "@/stores/modal";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import { ContextSrc } from "@/composables/enums";
|
||||
import OptionSvg from "@/assets/icons/more.svg";
|
||||
|
||||
import { ref } from "vue";
|
||||
import trackContext from "../../contexts/track_context";
|
||||
import { Track } from "../../interfaces";
|
||||
import { paths } from "../../config";
|
||||
import trackContext from "@/contexts/track_context";
|
||||
import { Track } from "@/interfaces";
|
||||
import { paths } from "@/config";
|
||||
|
||||
const contextStore = useContextStore();
|
||||
|
||||
const context_on = ref(false);
|
||||
const imguri = paths.images.thumb;
|
||||
const options_button_clicked = ref(false);
|
||||
|
||||
const showContextMenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
@@ -86,6 +96,7 @@ const showContextMenu = (e: Event) => {
|
||||
contextStore.$subscribe((mutation, state) => {
|
||||
if (!state.visible) {
|
||||
context_on.value = false;
|
||||
options_button_clicked.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -110,44 +121,42 @@ function emitUpdate(track: Track) {
|
||||
.songlist-item {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 0.25fr;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 2rem 2.5rem;
|
||||
height: 3.75rem;
|
||||
text-align: left;
|
||||
gap: $small;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
||||
.context {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
@include tablet-landscape {
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1fr 2.5rem;
|
||||
}
|
||||
|
||||
@include tablet-portrait {
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 2.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray4;
|
||||
|
||||
.options-icon {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.song-duration {
|
||||
@include tablet-landscape {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.song-album {
|
||||
.album {
|
||||
cursor: pointer;
|
||||
max-width: max-content;
|
||||
word-break: break-all;
|
||||
text-transform: capitalize;
|
||||
max-width: max-content;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@include tablet-portrait {
|
||||
@@ -156,6 +165,8 @@ function emitUpdate(track: Track) {
|
||||
}
|
||||
|
||||
.song-artists {
|
||||
word-break: break-all;
|
||||
|
||||
.artist {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -179,26 +190,63 @@ function emitUpdate(track: Track) {
|
||||
.song-duration {
|
||||
font-size: 0.9rem;
|
||||
width: 5rem !important;
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.options-icon {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1;
|
||||
width: 2rem;
|
||||
margin-right: 1rem;
|
||||
|
||||
svg {
|
||||
transition: all 0.2s ease-in;
|
||||
transform: rotate(90deg);
|
||||
stroke: $track-btn-svg;
|
||||
|
||||
circle {
|
||||
fill: $track-btn-svg;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray5;
|
||||
}
|
||||
}
|
||||
|
||||
.options_button_clicked {
|
||||
background-color: $gray5;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.flex {
|
||||
position: relative;
|
||||
padding-left: 4rem;
|
||||
align-items: center;
|
||||
|
||||
.thumbnail {
|
||||
margin-right: $small;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.album-art {
|
||||
position: absolute;
|
||||
left: $small;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-right: 1rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.now-playing-track {
|
||||
position: absolute;
|
||||
left: $small;
|
||||
top: $small;
|
||||
}
|
||||
|
||||
.title {
|
||||
cursor: pointer;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,11 +74,11 @@ const showContextMenu = (e: Event) => {
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "PlayThis", track: Track): void;
|
||||
(e: "PlayThis"): void;
|
||||
}>();
|
||||
|
||||
const playThis = (track: Track) => {
|
||||
emit("PlayThis", track);
|
||||
emit("PlayThis");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -122,7 +122,13 @@ const playThis = (track: Track) => {
|
||||
margin: 0 0.5rem 0 0;
|
||||
background-image: url(../../assets/images/null.webp);
|
||||
}
|
||||
|
||||
.title {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.artist {
|
||||
word-break: break-all;
|
||||
font-size: small;
|
||||
color: rgba(255, 255, 255, 0.637);
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import state from "./state";
|
||||
import { AlbumInfo, Track } from "../interfaces";
|
||||
|
||||
const getAlbumTracks = async (album: string, artist: string) => {
|
||||
let data = {
|
||||
info: <AlbumInfo>{},
|
||||
tracks: <Track[]>[],
|
||||
};
|
||||
|
||||
await axios
|
||||
.post(state.settings.uri + "/album/tracks", {
|
||||
album: album,
|
||||
artist: artist,
|
||||
})
|
||||
.then((res) => {
|
||||
data.info = res.data.info;
|
||||
data.tracks = res.data.songs;
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getAlbumArtists = async (album:string, artist:string) => {
|
||||
let artists = [];
|
||||
|
||||
await axios
|
||||
.post(state.settings.uri + "/album/artists", {
|
||||
album: album,
|
||||
artist: artist,
|
||||
})
|
||||
.then((res) => {
|
||||
artists = res.data.artists;
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
return artists;
|
||||
};
|
||||
|
||||
const getAlbumBio = async (album: string, albumartist: string) => {
|
||||
let bio = null;
|
||||
|
||||
await axios
|
||||
.post(state.settings.uri + "/album/bio", {
|
||||
album: album,
|
||||
albumartist: albumartist,
|
||||
})
|
||||
.then((res) => {
|
||||
bio = res.data.bio;
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
if (err.response.status === 404) {
|
||||
bio = null;
|
||||
}
|
||||
});
|
||||
|
||||
return bio;
|
||||
};
|
||||
|
||||
export { getAlbumTracks, getAlbumArtists, getAlbumBio };
|
||||
@@ -1,22 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { Folder, Track } from "../interfaces";
|
||||
import state from "./state";
|
||||
|
||||
export default async function (path: string) {
|
||||
let tracks = Array<Track>();
|
||||
let folders = Array<Folder>();
|
||||
|
||||
await axios
|
||||
.post(`${state.settings.uri}/folder`, {
|
||||
folder: path,
|
||||
})
|
||||
.then((res) => {
|
||||
tracks = res.data.tracks;
|
||||
folders = res.data.folders;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
return { tracks, folders };
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getElem } from "./perks";
|
||||
|
||||
export default (mouseX, mouseY) => {
|
||||
export default (mouseX: number, mouseY: number) => {
|
||||
const scope = getElem("app", "id");
|
||||
const contextMenu = getElem("context-menu", "class");
|
||||
// ? compute what is the mouse position relative to the container element
|
||||
@@ -0,0 +1,68 @@
|
||||
import state from "../state";
|
||||
import { AlbumInfo, Track } from "../../interfaces";
|
||||
import useAxios from "../useAxios";
|
||||
import { NotifType, useNotifStore } from "@/stores/notification";
|
||||
|
||||
const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
|
||||
const url = state.settings.uri + "/album";
|
||||
|
||||
interface AlbumData {
|
||||
info: AlbumInfo;
|
||||
tracks: Track[];
|
||||
}
|
||||
|
||||
const { data, status } = await useAxios({
|
||||
url,
|
||||
props: {
|
||||
hash: hash,
|
||||
},
|
||||
});
|
||||
|
||||
if (status == 204) {
|
||||
ToastStore().showNotification("Album not created yet!", NotifType.Error);
|
||||
return {
|
||||
info: {
|
||||
album: "",
|
||||
artist: "",
|
||||
colors: []
|
||||
},
|
||||
tracks: [],
|
||||
};
|
||||
}
|
||||
|
||||
return data as AlbumData;
|
||||
};
|
||||
|
||||
const getAlbumArtists = async (hash: string) => {
|
||||
const { data, error } = await useAxios({
|
||||
url: state.settings.uri + "/album/artists",
|
||||
props: {
|
||||
hash: hash,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return data.artists;
|
||||
};
|
||||
|
||||
const getAlbumBio = async (hash: string) => {
|
||||
const { data, status } = await useAxios({
|
||||
url: state.settings.uri + "/album/bio",
|
||||
props: {
|
||||
hash: hash,
|
||||
},
|
||||
});
|
||||
|
||||
if (data) {
|
||||
return data.bio;
|
||||
}
|
||||
|
||||
if (status == 404) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { getAlbumData as getAlbumTracks, getAlbumArtists, getAlbumBio };
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Folder, Track } from "@/interfaces";
|
||||
import state from "../state";
|
||||
import useAxios from "../useAxios";
|
||||
|
||||
export default async function (path: string) {
|
||||
interface FolderData {
|
||||
tracks: Track[];
|
||||
folders: Folder[];
|
||||
}
|
||||
|
||||
const { data, error } = await useAxios({
|
||||
url: `${state.settings.uri}/folder`,
|
||||
props: {
|
||||
folder: path,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return data as FolderData;
|
||||
}
|
||||
|
||||
return <FolderData>{
|
||||
tracks: [],
|
||||
folders: [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { Playlist, Track } from "../../interfaces";
|
||||
import { Notification, NotifType } from "../../stores/notification";
|
||||
import state from "../state";
|
||||
import useAxios from "../useAxios";
|
||||
/**
|
||||
* Creates a new playlist on the server.
|
||||
* @param playlist_name The name of the playlist to create.
|
||||
*/
|
||||
async function createNewPlaylist(playlist_name: string, track?: Track) {
|
||||
const { data, status } = await useAxios({
|
||||
url: state.settings.uri + "/playlist/new",
|
||||
props: {
|
||||
name: playlist_name,
|
||||
},
|
||||
});
|
||||
|
||||
if (status == 201) {
|
||||
new Notification("✅ Playlist created successfullly!");
|
||||
|
||||
if (track) {
|
||||
setTimeout(() => {
|
||||
addTrackToPlaylist(data.playlist, track);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
playlist: data.playlist as Playlist,
|
||||
};
|
||||
}
|
||||
|
||||
new Notification("That playlist already exists", NotifType.Error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
playlist: <Playlist>{},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all playlists from the server.
|
||||
* @returns {Promise<Playlist[]>} A promise that resolves to an array of playlists.
|
||||
*/
|
||||
async function getAllPlaylists(): Promise<Playlist[]> {
|
||||
const { data, error } = await useAxios({
|
||||
url: state.settings.uri + "/playlists",
|
||||
get: true,
|
||||
});
|
||||
|
||||
if (error) console.error(error);
|
||||
|
||||
if (data) {
|
||||
return data.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function addTrackToPlaylist(playlist: Playlist, track: Track) {
|
||||
const uri = `${state.settings.uri}/playlist/${playlist.playlistid}/add`;
|
||||
|
||||
const { status } = await useAxios({
|
||||
url: uri,
|
||||
props: {
|
||||
track: track.trackid,
|
||||
},
|
||||
});
|
||||
|
||||
if (status == 409) {
|
||||
new Notification("Track already exists in playlist", NotifType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
new Notification(track.title + " added to " + playlist.name);
|
||||
}
|
||||
|
||||
async function getPlaylist(pid: string) {
|
||||
const uri = state.settings.uri + "/playlist/" + pid;
|
||||
|
||||
interface PlaylistData {
|
||||
info: Playlist;
|
||||
tracks: Track[];
|
||||
}
|
||||
|
||||
const { data, error } = await useAxios({
|
||||
url: uri,
|
||||
get: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
new Notification("Something funny happened!", NotifType.Error);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return data as PlaylistData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function updatePlaylist(pid: string, playlist: FormData, pStore: any) {
|
||||
const uri = state.settings.uri + "/playlist/" + pid + "/update";
|
||||
|
||||
const { data, error } = await useAxios({
|
||||
url: uri,
|
||||
put: true,
|
||||
props: playlist,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
new Notification("Something funny happened!", NotifType.Error);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
pStore.updatePInfo(data.data);
|
||||
new Notification("Playlist updated!");
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
createNewPlaylist,
|
||||
getAllPlaylists,
|
||||
addTrackToPlaylist,
|
||||
getPlaylist,
|
||||
updatePlaylist,
|
||||
};
|
||||
@@ -37,9 +37,12 @@ function getElem(id: string, type: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts seconds into minutes and hours.
|
||||
* @param seconds The seconds to convert
|
||||
* @param long Whether to provide the time in the long format
|
||||
*/
|
||||
function formatSeconds(seconds: number, long?: boolean) {
|
||||
// check if there are arguments
|
||||
|
||||
const date = new Date(seconds * 1000);
|
||||
|
||||
const hh = date.getUTCHours();
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { Playlist, Track } from "../interfaces";
|
||||
import { Notification, NotifType } from "../stores/notification";
|
||||
import state from "./state";
|
||||
/**
|
||||
* Creates a new playlist on the server.
|
||||
* @param playlist_name The name of the playlist to create.
|
||||
*/
|
||||
async function createNewPlaylist(playlist_name: string, track?: Track) {
|
||||
let status = {
|
||||
success: false,
|
||||
playlist: <Playlist>{},
|
||||
};
|
||||
|
||||
await axios
|
||||
.post(state.settings.uri + "/playlist/new", {
|
||||
name: playlist_name,
|
||||
})
|
||||
.then((res) => {
|
||||
new Notification("✅ Playlist created successfullly!");
|
||||
|
||||
if (track) {
|
||||
setTimeout(() => {
|
||||
addTrackToPlaylist(res.data.playlist, track);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
status.success = true;
|
||||
status.playlist = res.data.playlist;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.status == 409) {
|
||||
new Notification(
|
||||
"That playlist already exists",
|
||||
NotifType.Error
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all playlists from the server.
|
||||
* @returns {Promise<Playlist[]>} A promise that resolves to an array of playlists.
|
||||
*/
|
||||
async function getAllPlaylists(): Promise<Playlist[]> {
|
||||
let playlists = <Playlist[]>[];
|
||||
|
||||
const newLocal = `${state.settings.uri}/playlists`;
|
||||
|
||||
await axios
|
||||
.get(newLocal)
|
||||
.then((res) => {
|
||||
playlists = res.data.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
return playlists;
|
||||
}
|
||||
|
||||
async function addTrackToPlaylist(playlist: Playlist, track: Track) {
|
||||
const uri = `${state.settings.uri}/playlist/${playlist.playlistid}/add`;
|
||||
|
||||
await axios
|
||||
.post(uri, { track: track.trackid })
|
||||
.then(() => {
|
||||
new Notification(track.title + " added to " + playlist.name);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response.status == 409) {
|
||||
new Notification("Track already exists in playlist", NotifType.Info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getPTracks(playlistid: string) {
|
||||
const uri = state.settings.uri + "/playlist/" + playlistid;
|
||||
|
||||
let tracks: Track[] = [];
|
||||
|
||||
await axios
|
||||
.get(uri)
|
||||
.then((res) => {
|
||||
tracks = res.data.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
new Notification("Something funny happened!", NotifType.Error);
|
||||
throw new Error(err);
|
||||
});
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
async function getPlaylist(pid: string) {
|
||||
const uri = state.settings.uri + "/playlist/" + pid;
|
||||
|
||||
let playlist = {
|
||||
info: {},
|
||||
tracks: <Track[]>[],
|
||||
};
|
||||
|
||||
await axios
|
||||
.get(uri)
|
||||
.then((res) => {
|
||||
playlist.info = res.data.info;
|
||||
playlist.tracks = res.data.tracks;
|
||||
})
|
||||
.catch((err) => {
|
||||
new Notification("Something funny happened!", NotifType.Error);
|
||||
throw new Error(err);
|
||||
});
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
async function updatePlaylist(pid: string, playlist: FormData, pStore: any) {
|
||||
const uri = state.settings.uri + "/playlist/" + pid + "/update";
|
||||
|
||||
await axios
|
||||
.put(uri, playlist, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
pStore.updatePInfo(res.data.data);
|
||||
new Notification("Playlist updated!");
|
||||
})
|
||||
.catch((err) => {
|
||||
new Notification("Something funny happened!", NotifType.Error);
|
||||
throw new Error(err);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
createNewPlaylist,
|
||||
getAllPlaylists,
|
||||
addTrackToPlaylist,
|
||||
getPTracks,
|
||||
getPlaylist,
|
||||
updatePlaylist,
|
||||
};
|
||||
@@ -9,29 +9,6 @@ const uris = {
|
||||
artists: `${base_url}/artists?q=`,
|
||||
};
|
||||
|
||||
async function search(query: string) {
|
||||
state.loading.value = true;
|
||||
|
||||
const url = base_url + encodeURIComponent(query.trim());
|
||||
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
const message = `An error has occured: ${res.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
state.loading.value = false;
|
||||
|
||||
return {
|
||||
tracks: data.data[0],
|
||||
albums: data.data[1],
|
||||
artists: data.data[2],
|
||||
};
|
||||
}
|
||||
|
||||
async function searchTracks(query: string) {
|
||||
const url = uris.tracks + encodeURIComponent(query.trim());
|
||||
|
||||
@@ -104,3 +81,6 @@ export {
|
||||
loadMoreAlbums,
|
||||
loadMoreArtists,
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// Rewrite this module using `useAxios` hook
|
||||
|
||||
@@ -1,57 +1,12 @@
|
||||
import { ref } from "@vue/reactivity";
|
||||
import { reactive } from "vue";
|
||||
import * as i from "../interfaces";
|
||||
|
||||
const search_query = ref("");
|
||||
|
||||
const queue = ref(
|
||||
Array<i.Track>({
|
||||
title: "Nothing played yet",
|
||||
artists: ["... blah blah blah"],
|
||||
image: "http://127.0.0.1:8900/images/thumbnails/4.webp",
|
||||
trackid: "",
|
||||
})
|
||||
);
|
||||
|
||||
const folder_song_list = ref([]);
|
||||
const folder_list = ref([]);
|
||||
|
||||
const current = ref(<i.Track>{
|
||||
title: "Nothing played yet",
|
||||
artists: ["... blah blah blah"],
|
||||
image: "http://127.0.0.1:8900/images/thumbnails/4.webp",
|
||||
trackid: "",
|
||||
});
|
||||
|
||||
const prev = ref(<i.Track>{
|
||||
title: "Nothing played yet",
|
||||
artists: ["... blah blah blah"],
|
||||
image: "http://127.0.0.1:8900/images/thumbnails/4.webp",
|
||||
trackid: "",
|
||||
});
|
||||
|
||||
const album = reactive({
|
||||
tracklist: Array<i.Track>(),
|
||||
info: <i.AlbumInfo>{},
|
||||
artists: Array<i.Artist>(),
|
||||
bio: "",
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const is_playing = ref(false);
|
||||
const settings = reactive({
|
||||
uri: "http://127.0.0.1:9876",
|
||||
});
|
||||
|
||||
export default {
|
||||
search_query,
|
||||
queue,
|
||||
folder_song_list,
|
||||
folder_list,
|
||||
current,
|
||||
prev,
|
||||
loading,
|
||||
is_playing,
|
||||
album,
|
||||
settings,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FetchProps } from "../interfaces";
|
||||
import axios, { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
export default async (args: FetchProps) => {
|
||||
let data: any = null;
|
||||
let error: string = null;
|
||||
let status: number = null;
|
||||
|
||||
function getAxios() {
|
||||
if (args.get) {
|
||||
return axios.get(args.url, args.props);
|
||||
}
|
||||
|
||||
if (args.put) {
|
||||
return axios.put(args.url, args.props, args.headers);
|
||||
}
|
||||
|
||||
return axios.post(args.url, args.props);
|
||||
}
|
||||
|
||||
await getAxios()
|
||||
.then((res: AxiosResponse) => {
|
||||
data = res.data;
|
||||
status = res.status;
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
error = err.message as string;
|
||||
status = err.response.status as number;
|
||||
});
|
||||
|
||||
return { data, error, status };
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import useQStore from "@/stores/queue"
|
||||
import useQStore from "@/stores/queue";
|
||||
|
||||
let key_down_fired = false;
|
||||
|
||||
@@ -9,12 +9,12 @@ function focusSearchBox() {
|
||||
}
|
||||
|
||||
export default function (queue: typeof useQStore) {
|
||||
const q = queue()
|
||||
window.addEventListener("keydown", (e: any) => {
|
||||
let target = e.target;
|
||||
const q = queue();
|
||||
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
let ctrlKey = e.ctrlKey;
|
||||
|
||||
function FocusedOnInput(target: any) {
|
||||
function FocusedOnInput(target: HTMLElement) {
|
||||
return target.tagName === "INPUT" || target.tagName === "TEXTAREA";
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,14 @@ export default function play(
|
||||
const f = store();
|
||||
|
||||
useQueue.playFromFolder(f.path, f.tracks);
|
||||
useQueue.play(f.tracks[0]);
|
||||
useQueue.play();
|
||||
break;
|
||||
case playSources.album:
|
||||
store = store as typeof album;
|
||||
const a = store();
|
||||
|
||||
useQueue.playFromAlbum(a.info.title, a.info.artist, a.tracks);
|
||||
useQueue.play(store().tracks[0]);
|
||||
useQueue.play();
|
||||
break;
|
||||
case playSources.playlist:
|
||||
store = store as typeof playlist;
|
||||
@@ -41,7 +41,7 @@ export default function play(
|
||||
if (p.tracks.length === 0) return;
|
||||
|
||||
useQueue.playFromPlaylist(p.info.name, p.info.playlistid, p.tracks);
|
||||
useQueue.play(store().tracks[0]);
|
||||
useQueue.play();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Playlist, Track } from "../interfaces";
|
||||
import Router from "../router";
|
||||
import { Option } from "../interfaces";
|
||||
import { getAllPlaylists, addTrackToPlaylist } from "../composables/playlists";
|
||||
import {
|
||||
getAllPlaylists,
|
||||
addTrackToPlaylist,
|
||||
} from "../composables/pages/playlists";
|
||||
|
||||
import useQueueStore from "../stores/queue";
|
||||
import useModalStore from "../stores/modal";
|
||||
@@ -15,7 +18,7 @@ import useModalStore from "../stores/modal";
|
||||
export default async (
|
||||
track: Track,
|
||||
modalStore: typeof useModalStore,
|
||||
QueueStore: typeof useQueueStore,
|
||||
QueueStore: typeof useQueueStore
|
||||
): Promise<Option[]> => {
|
||||
const separator: Option = {
|
||||
type: "separator",
|
||||
@@ -79,7 +82,7 @@ export default async (
|
||||
QueueStore().playTrackNext(track);
|
||||
},
|
||||
icon: "add_to_queue",
|
||||
}
|
||||
};
|
||||
|
||||
const go_to_folder: Option = {
|
||||
label: "Go to Folder",
|
||||
@@ -114,7 +117,7 @@ export default async (
|
||||
action: () => {
|
||||
Router.push({
|
||||
name: "AlbumView",
|
||||
params: { album: track.album, artist: track.albumartist },
|
||||
params: { hash: track.albumhash },
|
||||
});
|
||||
},
|
||||
icon: "album",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { FromOptions, NotifType } from "./composables/enums";
|
||||
|
||||
interface Track {
|
||||
export interface Track {
|
||||
trackid: string;
|
||||
title: string;
|
||||
album?: string;
|
||||
artists: string[];
|
||||
albumartist?: string;
|
||||
albumhash?: string;
|
||||
folder?: string;
|
||||
filepath?: string;
|
||||
length?: number;
|
||||
@@ -14,16 +15,19 @@ interface Track {
|
||||
image: string;
|
||||
tracknumber?: number;
|
||||
disknumber?: number;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
interface Folder {
|
||||
export interface Folder {
|
||||
name: string;
|
||||
path: string;
|
||||
trackcount: number;
|
||||
subdircount: number;
|
||||
is_sym: boolean;
|
||||
}
|
||||
|
||||
interface AlbumInfo {
|
||||
export interface AlbumInfo {
|
||||
albumid: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
count: number;
|
||||
@@ -33,23 +37,25 @@ interface AlbumInfo {
|
||||
is_compilation: boolean;
|
||||
is_soundtrack: boolean;
|
||||
is_single: boolean;
|
||||
hash: string;
|
||||
colors: string[];
|
||||
}
|
||||
|
||||
interface Artist {
|
||||
export interface Artist {
|
||||
name: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
export interface Option {
|
||||
type?: string;
|
||||
label?: string;
|
||||
action?: Function;
|
||||
action?: () => void;
|
||||
children?: Option[] | false;
|
||||
icon?: string;
|
||||
critical?: Boolean;
|
||||
}
|
||||
|
||||
interface Playlist {
|
||||
export interface Playlist {
|
||||
playlistid: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -58,51 +64,45 @@ interface Playlist {
|
||||
count?: number;
|
||||
lastUpdated?: string;
|
||||
thumb?: string;
|
||||
duration?: number
|
||||
}
|
||||
|
||||
interface Notif {
|
||||
export interface Notif {
|
||||
text: string;
|
||||
type: NotifType;
|
||||
}
|
||||
|
||||
interface fromFolder {
|
||||
export interface fromFolder {
|
||||
type: FromOptions;
|
||||
path: string;
|
||||
name: string;
|
||||
}
|
||||
interface fromAlbum {
|
||||
export interface fromAlbum {
|
||||
type: FromOptions;
|
||||
name: string;
|
||||
albumartist: string;
|
||||
}
|
||||
interface fromPlaylist {
|
||||
export interface fromPlaylist {
|
||||
type: FromOptions;
|
||||
name: string;
|
||||
playlistid: string;
|
||||
}
|
||||
|
||||
interface fromSearch {
|
||||
export interface fromSearch {
|
||||
type: FromOptions;
|
||||
query: string;
|
||||
}
|
||||
|
||||
interface subPath {
|
||||
export interface subPath {
|
||||
name: string;
|
||||
path: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export {
|
||||
Track,
|
||||
Folder,
|
||||
AlbumInfo,
|
||||
Artist,
|
||||
Option,
|
||||
Playlist,
|
||||
Notif,
|
||||
fromFolder,
|
||||
fromAlbum,
|
||||
fromPlaylist,
|
||||
fromSearch,
|
||||
subPath,
|
||||
};
|
||||
export interface FetchProps {
|
||||
url: string;
|
||||
props?: {};
|
||||
get?: boolean;
|
||||
put?: boolean;
|
||||
headers?: {};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import Home from "@/views/Home.vue";
|
||||
import FolderView from "@/views/FolderView.vue";
|
||||
import PlaylistView from "@/views/PlaylistView.vue";
|
||||
import Playlists from "@/views/Playlists.vue";
|
||||
|
||||
import state from "@/composables/state";
|
||||
import useAStore from "@/stores/pages/album";
|
||||
import useFStore from "@/stores/pages/folder";
|
||||
import usePTrackStore from "@/stores/pages/playlist";
|
||||
import usePStore from "@/stores/pages/playlists";
|
||||
import AlbumsExplorer from "@/views/AlbumsExplorer.vue";
|
||||
import AlbumView from "@/views/AlbumView.vue";
|
||||
|
||||
import ArtistsExplorer from "@/views/ArtistsExplorer.vue";
|
||||
import FolderView from "@/views/FolderView.vue";
|
||||
import Home from "@/views/Home.vue";
|
||||
import Playlists from "@/views/Playlists.vue";
|
||||
import PlaylistView from "@/views/PlaylistView.vue";
|
||||
import SettingsView from "@/views/SettingsView.vue";
|
||||
|
||||
import usePStore from "@/stores/pages/playlists";
|
||||
import usePTrackStore from "@/stores/pages/playlist";
|
||||
import useFStore from "@/stores/pages/folder";
|
||||
import useAStore from "@/stores/pages/album";
|
||||
import state from "@/composables/state";
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -62,17 +59,14 @@ const routes = [
|
||||
component: AlbumsExplorer,
|
||||
},
|
||||
{
|
||||
path: "/albums/:album/:artist",
|
||||
path: "/albums/:hash",
|
||||
name: "AlbumView",
|
||||
component: AlbumView,
|
||||
beforeEnter: async (to) => {
|
||||
state.loading.value = true;
|
||||
await useAStore().fetchTracksAndArtists(
|
||||
to.params.album,
|
||||
to.params.artist
|
||||
);
|
||||
await useAStore().fetchTracksAndArtists(to.params.hash);
|
||||
state.loading.value = false;
|
||||
useAStore().fetchBio(to.params.album, to.params.artist);
|
||||
useAStore().fetchBio(to.params.hash);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useNotifStore } from "../notification";
|
||||
import { Track, Artist, AlbumInfo } from "../../interfaces";
|
||||
import {
|
||||
getAlbumTracks,
|
||||
getAlbumArtists,
|
||||
getAlbumBio,
|
||||
} from "../../composables/album";
|
||||
} from "../../composables/pages/album";
|
||||
|
||||
function sortTracks(tracks: Track[]) {
|
||||
return tracks.sort((a, b) => {
|
||||
if (a.tracknumber && b.tracknumber) {
|
||||
return a.tracknumber - b.tracknumber;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export default defineStore("album", {
|
||||
state: () => ({
|
||||
@@ -17,25 +28,23 @@ export default defineStore("album", {
|
||||
/**
|
||||
* Fetches a single album information, artists and its tracks from the server
|
||||
* using the title and album-artist of the album.
|
||||
* @param title title of the album
|
||||
* @param albumartist artist of the album
|
||||
* @param hash title of the album
|
||||
*/
|
||||
async fetchTracksAndArtists(title: string, albumartist: string) {
|
||||
const tracks = await getAlbumTracks(title, albumartist);
|
||||
const artists = await getAlbumArtists(title, albumartist);
|
||||
async fetchTracksAndArtists(hash: string) {
|
||||
const tracks = await getAlbumTracks(hash, useNotifStore);
|
||||
const artists = await getAlbumArtists(hash);
|
||||
|
||||
this.tracks = tracks.tracks;
|
||||
this.tracks = sortTracks(tracks.tracks);
|
||||
this.info = tracks.info;
|
||||
this.artists = artists;
|
||||
},
|
||||
/**
|
||||
* Fetches the album bio from the server
|
||||
* @param title title of the album
|
||||
* @param albumartist artist of the album
|
||||
* @param {string} hash title of the album
|
||||
*/
|
||||
fetchBio(title: string, albumartist: string) {
|
||||
fetchBio(hash: string) {
|
||||
this.bio = null;
|
||||
getAlbumBio(title, albumartist).then((bio) => {
|
||||
getAlbumBio(hash).then((bio) => {
|
||||
this.bio = bio;
|
||||
});
|
||||
},
|
||||
|
||||