fix merge conflicts

This commit is contained in:
geoffrey45
2022-07-06 17:49:19 +03:00
56 changed files with 819 additions and 612 deletions
-1
View File
@@ -3,7 +3,6 @@ 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, 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. checking and creating config dirs and starting the re-indexing process using a background thread.
""" """
from app import functions from app import functions
from app import helpers from app import helpers
from app import prep from app import prep
+20 -11
View File
@@ -6,12 +6,12 @@ from typing import List
from app import api from app import api
from app import helpers from app import helpers
from app import instances
from app import models from app import models
from app.functions import FetchAlbumBio
from app.lib import albumslib from app.lib import albumslib
from flask import Blueprint from flask import Blueprint
from flask import request from flask import request
from app.functions import FetchAlbumBio
from app import instances
album_bp = Blueprint("album", __name__, url_prefix="") album_bp = Blueprint("album", __name__, url_prefix="")
@@ -41,27 +41,31 @@ def get_album():
"""Returns all the tracks in the given album.""" """Returns all the tracks in the given album."""
data = request.get_json() data = request.get_json()
albumhash = data["hash"] albumhash = data["hash"]
error_msg = {"error": "Album not created yet."}
tracks = instances.tracks_instance.find_tracks_by_hash(albumhash) tracks = instances.tracks_instance.find_tracks_by_hash(albumhash)
if len(tracks) == 0:
return error_msg, 204
tracks = [models.Track(t) for t in tracks] tracks = [models.Track(t) for t in tracks]
tracks = helpers.RemoveDuplicates(tracks)() tracks = helpers.RemoveDuplicates(tracks)()
album = instances.album_instance.find_album_by_hash(albumhash) album = instances.album_instance.find_album_by_hash(albumhash)
if not album: if not album:
return {"error": "Album not created yet."}, 204 return error_msg, 204
album = models.Album(album) album = models.Album(album)
album.count = len(tracks) album.count = len(tracks)
album.duration = albumslib.get_album_duration(tracks) try:
album.duration = sum([t.length for t in tracks])
except AttributeError:
album.duration = 0
if ( if (album.count == 1 and tracks[0].title == album.title
album.count == 1 and tracks[0].tracknumber == 1 and tracks[0].disknumber == 1):
and tracks[0].title == album.title
and tracks[0].tracknumber == 1
and tracks[0].disknumber == 1
):
album.is_single = True album.is_single = True
return {"tracks": tracks, "info": album} return {"tracks": tracks, "info": album}
@@ -72,12 +76,17 @@ def get_album_bio():
"""Returns the album bio for the given album.""" """Returns the album bio for the given album."""
data = request.get_json() data = request.get_json()
album_hash = data["hash"] album_hash = data["hash"]
err_msg = {"bio": "No bio found"}
album = instances.album_instance.find_album_by_hash(album_hash) album = instances.album_instance.find_album_by_hash(album_hash)
if album is None:
return err_msg, 404
bio = FetchAlbumBio(album["title"], album["artist"])() bio = FetchAlbumBio(album["title"], album["artist"])()
if bio is None: if bio is None:
return {"bio": "No bio found."}, 404 return err_msg, 404
return {"bio": bio} return {"bio": bio}
-1
View File
@@ -10,7 +10,6 @@ from flask import Blueprint
artist_bp = Blueprint("artist", __name__, url_prefix="/") artist_bp = Blueprint("artist", __name__, url_prefix="/")
# @artist_bp.route("/artist/<artist>") # @artist_bp.route("/artist/<artist>")
# @cache.cached() # @cache.cached()
# def get_artist_data(artist: str): # def get_artist_data(artist: str):
+11 -4
View File
@@ -8,12 +8,13 @@ from app import exceptions
from app import instances from app import instances
from app import models from app import models
from app import serializer 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 app.lib import playlistlib
from flask import Blueprint from flask import Blueprint
from flask import request from flask import request
from app.helpers import Get, UseBisection, create_new_date
playlist_bp = Blueprint("playlist", __name__, url_prefix="/") playlist_bp = Blueprint("playlist", __name__, url_prefix="/")
PlaylistExists = exceptions.PlaylistExists PlaylistExists = exceptions.PlaylistExists
@@ -27,7 +28,8 @@ def get_all_playlists():
dbplaylists = [models.Playlist(p) for p in dbplaylists] dbplaylists = [models.Playlist(p) for p in dbplaylists]
playlists = [ playlists = [
serializer.Playlist(p, construct_last_updated=False) for p in dbplaylists serializer.Playlist(p, construct_last_updated=False)
for p in dbplaylists
] ]
playlists.sort( playlists.sort(
key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"), key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"),
@@ -84,7 +86,12 @@ def get_playlist(playlistid: str):
playlist = models.Playlist(p) playlist = models.Playlist(p)
tracks = playlistlib.create_playlist_tracks(playlist.pretracks) tracks = playlistlib.create_playlist_tracks(playlist.pretracks)
return {"info": serializer.Playlist(playlist), "tracks": tracks}
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"]) @playlist_bp.route("/playlist/<playlistid>/update", methods=["PUT"])
+6 -6
View File
@@ -3,14 +3,14 @@ Contains all the search routes.
""" """
from pprint import pprint from pprint import pprint
from typing import List from typing import List
from app import helpers from app import helpers
from app import models
from app import serializer
from app.lib import searchlib from app.lib import searchlib
from flask import Blueprint from flask import Blueprint
from flask import request from flask import request
from app import models
from app import serializer
search_bp = Blueprint("search", __name__, url_prefix="/") search_bp = Blueprint("search", __name__, url_prefix="/")
SEARCH_RESULTS = { SEARCH_RESULTS = {
@@ -197,20 +197,20 @@ def search_load_more():
if type == "tracks": if type == "tracks":
t = SearchResults.tracks t = SearchResults.tracks
return { return {
"tracks": t[index : index + 5], "tracks": t[index:index + 5],
"more": len(t) > index + 5, "more": len(t) > index + 5,
} }
elif type == "albums": elif type == "albums":
a = SearchResults.albums a = SearchResults.albums
return { return {
"albums": a[index : index + 6], "albums": a[index:index + 6],
"more": len(a) > index + 6, "more": len(a) > index + 6,
} }
elif type == "artists": elif type == "artists":
a = SearchResults.artists a = SearchResults.artists
return { return {
"artists": a[index : index + 6], "artists": a[index:index + 6],
"more": len(a) > index + 6, "more": len(a) > index + 6,
} }
+9 -7
View File
@@ -3,11 +3,10 @@ Contains all the track routes.
""" """
from app import api from app import api
from app import instances from app import instances
from app import models
from flask import Blueprint from flask import Blueprint
from flask import send_file from flask import send_file
from app import models
track_bp = Blueprint("track", __name__, url_prefix="/") track_bp = Blueprint("track", __name__, url_prefix="/")
@@ -17,14 +16,18 @@ def send_track_file(trackid):
Returns an audio file that matches the passed id to the client. Returns an audio file that matches the passed id to the client.
""" """
track = instances.tracks_instance.get_track_by_id(trackid) track = instances.tracks_instance.get_track_by_id(trackid)
msg = {"msg": "File Not Found"}
if track is None: if track is None:
return "File not found", 404 return msg, 404
track = models.Track(track) track = models.Track(track)
type = track.filepath.split(".")[-1] type = track.filepath.split(".")[-1]
return send_file(track.filepath, mimetype=f"audio/{type}") try:
return send_file(track.filepath, mimetype=f"audio/{type}")
except FileNotFoundError:
return msg, 404
@track_bp.route("/sample") @track_bp.route("/sample")
@@ -33,6 +36,5 @@ def get_sample_track():
Returns a sample track object. Returns a sample track object.
""" """
return instances.tracks_instance.get_song_by_album( return instances.tracks_instance.get_song_by_album("Legends Never Die",
"Legends Never Die", "Juice WRLD" "Juice WRLD")
)
+20 -2
View File
@@ -2,6 +2,8 @@
This file contains the Album class for interacting with This file contains the Album class for interacting with
album documents in MongoDB. album documents in MongoDB.
""" """
from typing import List
from app.db.mongodb import convert_many from app.db.mongodb import convert_many
from app.db.mongodb import convert_one from app.db.mongodb import convert_one
from app.db.mongodb import MongoAlbums from app.db.mongodb import MongoAlbums
@@ -20,8 +22,13 @@ class Albums(MongoAlbums):
""" """
album = album.__dict__ album = album.__dict__
return self.collection.update_one( return self.collection.update_one(
{"album": album["title"], "artist": album["artist"]}, {
{"$set": album}, "album": album["title"],
"artist": album["artist"]
},
{
"$set": album
},
upsert=True, upsert=True,
).upserted_id ).upserted_id
@@ -59,3 +66,14 @@ class Albums(MongoAlbums):
""" """
album = self.collection.find_one({"hash": hash}) album = self.collection.find_one({"hash": hash})
return convert_one(album) 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
}},
)
+13 -5
View File
@@ -1,10 +1,10 @@
""" """
This file contains the Playlists class for interacting with the playlist documents in MongoDB. 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_many
from app.db.mongodb import convert_one from app.db.mongodb import convert_one
from app.db.mongodb import MongoPlaylists from app.db.mongodb import MongoPlaylists
from app import helpers
from bson import ObjectId from bson import ObjectId
@@ -18,8 +18,12 @@ class Playlists(MongoPlaylists):
Inserts a new playlist object into the database. Inserts a new playlist object into the database.
""" """
return self.collection.update_one( return self.collection.update_one(
{"name": playlist["name"]}, {
{"$set": playlist}, "name": playlist["name"]
},
{
"$set": playlist
},
upsert=True, upsert=True,
).upserted_id ).upserted_id
@@ -45,7 +49,9 @@ class Playlists(MongoPlaylists):
return self.collection.update_one( return self.collection.update_one(
{"_id": ObjectId(playlistid)}, {"_id": ObjectId(playlistid)},
{"$set": {"lastUpdated": date}}, {"$set": {
"lastUpdated": date
}},
) )
def add_track_to_playlist(self, playlistid: str, track: dict) -> None: def add_track_to_playlist(self, playlistid: str, track: dict) -> None:
@@ -56,7 +62,9 @@ class Playlists(MongoPlaylists):
{ {
"_id": ObjectId(playlistid), "_id": ObjectId(playlistid),
}, },
{"$push": {"pre_tracks": track}}, {"$push": {
"pre_tracks": track
}},
) )
self.set_last_updated(playlistid) self.set_last_updated(playlistid)
+42 -18
View File
@@ -2,7 +2,9 @@
This file contains the AllSongs class for interacting with track documents in MongoDB. This file contains the AllSongs class for interacting with track documents in MongoDB.
""" """
import pymongo import pymongo
from app.db.mongodb import MongoTracks, convert_many, convert_one from app.db.mongodb import convert_many
from app.db.mongodb import convert_one
from app.db.mongodb import MongoTracks
from bson import ObjectId from bson import ObjectId
@@ -18,9 +20,12 @@ class Tracks(MongoTracks):
""" """
Inserts a new track object into the database. Inserts a new track object into the database.
""" """
return self.collection.update_one( return self.collection.update_one({
{"filepath": song_obj["filepath"]}, {"$set": song_obj}, upsert=True "filepath": song_obj["filepath"]
).upserted_id }, {
"$set": song_obj
},
upsert=True).upserted_id
def insert_many(self, songs: list): def insert_many(self, songs: list):
""" """
@@ -52,21 +57,33 @@ class Tracks(MongoTracks):
""" """
Returns all the songs matching the albums in the query params (using regex). 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) return convert_many(songs)
def search_songs_by_artist(self, query: str) -> list: def search_songs_by_artist(self, query: str) -> list:
""" """
Returns all the songs matching the artists in the query params. 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) return convert_many(songs)
def find_song_by_title(self, query: str) -> list: def find_song_by_title(self, query: str) -> list:
""" """
Finds all the tracks matching the title in the query params. 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) return convert_many(song)
def find_songs_by_album(self, name: str, artist: str) -> list: def find_songs_by_album(self, name: str, artist: str) -> list:
@@ -80,7 +97,9 @@ class Tracks(MongoTracks):
""" """
Returns a sorted list of all the tracks exactly matching the folder in the query params 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) return convert_many(songs)
def find_songs_by_filenames(self, filenames: list) -> list: def find_songs_by_filenames(self, filenames: list) -> list:
@@ -102,8 +121,10 @@ class Tracks(MongoTracks):
Returns a list of all the tracks matching the path in the query params. Returns a list of all the tracks matching the path in the query params.
""" """
return self.collection.count_documents( return self.collection.count_documents(
{"filepath": {"$regex": f"^{path}", "$options": "i"}} {"filepath": {
) "$regex": f"^{path}",
"$options": "i"
}})
def find_songs_by_artist(self, query: str) -> list: def find_songs_by_artist(self, query: str) -> list:
""" """
@@ -117,8 +138,10 @@ class Tracks(MongoTracks):
Returns a list of all the tracks containing the albumartist in the query params. Returns a list of all the tracks containing the albumartist in the query params.
""" """
songs = self.collection.find( songs = self.collection.find(
{"albumartist": {"$regex": query, "$options": "i"}} {"albumartist": {
) "$regex": query,
"$options": "i"
}})
return convert_many(songs) return convert_many(songs)
def get_song_by_path(self, path: str) -> dict: def get_song_by_path(self, path: str) -> dict:
@@ -155,13 +178,14 @@ class Tracks(MongoTracks):
songs = self.collection.find({"albumhash": hash}) songs = self.collection.find({"albumhash": hash})
return convert_many(songs) return convert_many(songs)
def find_track_by_title_artists_album( def find_track_by_title_artists_album(self, title: str, artist: str,
self, title: str, artist: str, album: str album: str) -> dict:
) -> dict:
""" """
Returns a single track matching the title, artist, and album in the query params. Returns a single track matching the title, artist, and album in the query params.
""" """
song = self.collection.find_one( song = self.collection.find_one({
{"title": title, "artists": artist, "album": album} "title": title,
) "artists": artist,
"album": album
})
return convert_one(song) return convert_one(song)
+43 -30
View File
@@ -7,14 +7,19 @@ from concurrent.futures import ThreadPoolExecutor
from io import BytesIO from io import BytesIO
import requests import requests
from PIL import Image from app import helpers
from app import settings
from app import helpers, settings from app.lib import trackslib
from app.lib import watchdoge from app.lib import watchdoge
from app.lib.albumslib import ValidateAlbumThumbs from app.lib.albumslib import ValidateAlbumThumbs
from app.lib import trackslib from app.lib.colorlib import ProcessAlbumColors
from app.lib.populate import CreateAlbums, Populate
from app.lib.playlistlib import ValidatePlaylistThumbs 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
log = get_logger()
@helpers.background @helpers.background
@@ -22,18 +27,25 @@ def run_checks():
""" """
Checks for new songs every 5 minutes. Checks for new songs every 5 minutes.
""" """
# while True:
trackslib.validate_tracks()
Populate()
CreateAlbums()
if helpers.Ping()():
CheckArtistImages()()
ValidateAlbumThumbs() ValidateAlbumThumbs()
ValidatePlaylistThumbs()
while True:
trackslib.validate_tracks()
Populate()
CreateAlbums()
if helpers.Ping()():
CheckArtistImages()()
@helpers.background
def process_album_colors():
ProcessAlbumColors()
ValidatePlaylistThumbs()
process_album_colors()
time.sleep(300)
@helpers.background @helpers.background
@@ -67,6 +79,7 @@ class getArtistImage:
class useImageDownloader: class useImageDownloader:
def __init__(self, url: str, dest: str) -> None: def __init__(self, url: str, dest: str) -> None:
self.url = url self.url = url
self.dest = dest self.dest = dest
@@ -76,14 +89,18 @@ class useImageDownloader:
img = Image.open(BytesIO(requests.get(self.url).content)) img = Image.open(BytesIO(requests.get(self.url).content))
img.save(self.dest, format="webp") img.save(self.dest, format="webp")
img.close() img.close()
return "fetched image"
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
time.sleep(5) time.sleep(5)
return "connection error"
class CheckArtistImages: class CheckArtistImages:
def __init__(self): def __init__(self):
self.artists: list[str] = [] self.artists: list[str] = []
print("Checking for artist images") print("Checking for artist images")
log.info("Checking artist images")
@staticmethod @staticmethod
def check_if_exists(img_path: str): def check_if_exists(img_path: str):
@@ -104,29 +121,25 @@ class CheckArtistImages:
:param artistname: The artist name :param artistname: The artist name
""" """
img_path = ( img_path = (settings.APP_DIR + "/images/artists/" +
settings.APP_DIR helpers.create_safe_name(artistname) + ".webp")
+ "/images/artists/"
+ helpers.create_safe_name(artistname)
+ ".webp"
)
if cls.check_if_exists(img_path): if cls.check_if_exists(img_path):
return return "exists"
url = getArtistImage(artistname)() url = getArtistImage(artistname)()
if url is None: if url is None:
return return "url is none"
useImageDownloader(url, img_path)()
return useImageDownloader(url, img_path)()
def __call__(self): def __call__(self):
self.artists = helpers.Get.get_all_artists() self.artists = helpers.Get.get_all_artists()
with ThreadPoolExecutor() as pool: with ThreadPoolExecutor() as pool:
iter = pool.map(self.download_image, self.artists) iter = pool.map(self.download_image, self.artists)
for i in iter: [i for i in iter]
pass
print("Done fetching images") print("Done fetching images")
@@ -136,8 +149,7 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None:
Returns the album bio for a given album. 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( 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: try:
response = requests.get(last_fm_url) response = requests.get(last_fm_url)
@@ -146,7 +158,8 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None:
return None return None
try: 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: except KeyError:
bio = None bio = None
+9 -14
View File
@@ -1,19 +1,16 @@
""" """
This module contains mini functions for the server. This module contains mini functions for the server.
""" """
from dataclasses import dataclass
import os import os
from pprint import pprint
import threading import threading
from datetime import datetime from datetime import datetime
from typing import Dict, Set from typing import Dict
from typing import List from typing import List
from typing import Set
import requests import requests
from app import models
from app import instances from app import instances
from app.lib.albumslib import Thumbnail from app import models
def background(func): def background(func):
@@ -53,9 +50,8 @@ def run_fast_scandir(__dir: str, full=False) -> Dict[List[str], List[str]]:
return subfolders, files return subfolders, files
class RemoveDuplicates: class RemoveDuplicates:
def __init__(self, tracklist: List[models.Track]) -> None: def __init__(self, tracklist: List[models.Track]) -> None:
self.tracklist = tracklist self.tracklist = tracklist
@@ -77,15 +73,12 @@ def is_valid_file(filename: str) -> bool:
return False return False
ill_chars = '/\\:*?"<>|#&'
def create_album_hash(title: str, artist: str) -> str: def create_album_hash(title: str, artist: str) -> str:
""" """
Creates a simple hash for an album Creates a simple hash for an album
""" """
lower = (title + artist).replace(" ", "").lower() lower = (title + artist).replace(" ", "").lower()
hash = "".join([i for i in lower if i not in ill_chars]) hash = "".join([i for i in lower if i.isalnum()])
return hash return hash
@@ -99,7 +92,7 @@ def create_safe_name(name: str) -> str:
""" """
Creates a url-safe name from a name. Creates a url-safe name from a name.
""" """
return "".join([i for i in name if i not in ill_chars]) return "".join([i for i in name if i.isalnum()])
class UseBisection: class UseBisection:
@@ -110,7 +103,8 @@ class UseBisection:
items. items.
""" """
def __init__(self, source: List, search_from: str, queries: List[str]) -> None: def __init__(self, source: List, search_from: str,
queries: List[str]) -> None:
self.source_list = source self.source_list = source
self.queries_list = queries self.queries_list = queries
self.attr = search_from self.attr = search_from
@@ -140,6 +134,7 @@ class UseBisection:
class Get: class Get:
@staticmethod @staticmethod
def get_all_tracks() -> List[models.Track]: def get_all_tracks() -> List[models.Track]:
""" """
+7 -15
View File
@@ -6,7 +6,9 @@ import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import List
from app import helpers, instances, models from app import helpers
from app import instances
from app import models
from app.lib import taglib from app.lib import taglib
from app.logger import logg from app.logger import logg
from app.settings import THUMBS_PATH from app.settings import THUMBS_PATH
@@ -35,6 +37,7 @@ class RipAlbumImage:
class ValidateAlbumThumbs: class ValidateAlbumThumbs:
@staticmethod @staticmethod
def remove_obsolete(): def remove_obsolete():
""" """
@@ -58,7 +61,9 @@ class ValidateAlbumThumbs:
Re-rip lost album thumbnails Re-rip lost album thumbnails
""" """
entries = os.scandir(THUMBS_PATH) entries = os.scandir(THUMBS_PATH)
entries = [Thumbnail(entry.name) for entry in entries if entry.is_file()] entries = [
Thumbnail(entry.name) for entry in entries if entry.is_file()
]
albums = helpers.Get.get_all_albums() albums = helpers.Get.get_all_albums()
thumbs = [(album.hash + ".webp") for album in albums] thumbs = [(album.hash + ".webp") for album in albums]
@@ -85,19 +90,6 @@ class ValidateAlbumThumbs:
self.find_lost_thumbnails() self.find_lost_thumbnails()
def get_album_duration(album: List[models.Track]) -> int:
"""
Gets the duration of an album.
"""
album_duration = 0
for track in album:
album_duration += track.length
return album_duration
def use_defaults() -> str: def use_defaults() -> str:
""" """
Returns a path to a random image in the defaults directory. Returns a path to a random image in the defaults directory.
+23 -27
View File
@@ -1,17 +1,17 @@
from io import BytesIO
import colorgram import colorgram
from app import api
from app import instances from app import instances
from app.lib.taglib import return_album_art from app import settings
from PIL import Image from app.helpers import Get
from progress.bar import Bar 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.""" """Extracts 2 of the most dominant colors from an image."""
try: 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: except OSError:
return [] return []
@@ -24,30 +24,26 @@ def get_image_colors(image) -> list:
return formatted_colors return formatted_colors
def save_track_colors(img, filepath) -> None: class ProcessAlbumColors:
"""Saves the track colors to the database"""
track_colors = get_image_colors(img) def __init__(self) -> None:
log.info("Processing album colors")
all_albums = Get.get_all_albums()
tc_dict = { all_albums = [a for a in all_albums if len(a.colors) == 0]
"filepath": filepath,
"colors": track_colors,
}
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(): @staticmethod
_bar = Bar("Processing image colors", max=len(api.DB_TRACKS)) def process_color(album: Album):
img = settings.THUMBS_PATH + "/" + album.image
for track in api.DB_TRACKS: colors = get_image_colors(img)
filepath = track["filepath"]
album_art = return_album_art(filepath)
if album_art is not None: if len(colors) > 0:
img = Image.open(BytesIO(album_art)) instances.album_instance.set_album_colors(colors, album.hash)
save_track_colors(img, filepath)
_bar.next() return colors
_bar.finish()
+12 -9
View File
@@ -1,13 +1,12 @@
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass from dataclasses import dataclass
from os import scandir from os import scandir
import time
from typing import Tuple from typing import Tuple
from concurrent.futures import ThreadPoolExecutor
from app.models import Folder
from app.models import Track
from app import instances from app import instances
from app.models import Folder
from app.models import Track
@dataclass @dataclass
@@ -27,10 +26,14 @@ def get_folder_track_count(path: str) -> int:
def create_folder(dir: Dir) -> Folder: def create_folder(dir: Dir) -> Folder:
"""Create a single Folder object""" """Create a single Folder object"""
folder = { folder = {
"name": dir.path.split("/")[-1], "name":
"path": dir.path, dir.path.split("/")[-1],
"is_sym": dir.is_sym, "path":
"trackcount": instances.tracks_instance.find_tracks_inside_path_regex(dir.path), dir.path,
"is_sym":
dir.is_sym,
"trackcount":
instances.tracks_instance.find_tracks_inside_path_regex(dir.path),
} }
return Folder(folder) return Folder(folder)
+9 -7
View File
@@ -11,14 +11,13 @@ from app import exceptions
from app import instances from app import instances
from app import models from app import models
from app import settings 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 Image
from PIL import ImageSequence from PIL import ImageSequence
from werkzeug import datastructures from werkzeug import datastructures
from app.lib import trackslib
from app.helpers import Get
from app.logger import get_logger
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
logg = get_logger() logg = get_logger()
@@ -53,7 +52,8 @@ def create_thumbnail(image: any, img_path: str) -> str:
Creates a 250 x 250 thumbnail from a playlist image Creates a 250 x 250 thumbnail from a playlist image
""" """
thumb_path = "thumb_" + img_path thumb_path = "thumb_" + img_path
full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists", thumb_path) full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists",
thumb_path)
aspect_ratio = image.width / image.height aspect_ratio = image.width / image.height
@@ -71,11 +71,13 @@ def save_p_image(file: datastructures.FileStorage, pid: str):
""" """
img = Image.open(file) img = Image.open(file)
random_str = "".join(random.choices(string.ascii_letters + string.digits, k=5)) random_str = "".join(
random.choices(string.ascii_letters + string.digits, k=5))
img_path = pid + str(random_str) + ".webp" img_path = pid + str(random_str) + ".webp"
full_img_path = os.path.join(settings.APP_DIR, "images", "playlists", img_path) full_img_path = os.path.join(settings.APP_DIR, "images", "playlists",
img_path)
if file.content_type == "image/gif": if file.content_type == "image/gif":
frames = [] frames = []
+12 -8
View File
@@ -1,20 +1,22 @@
from dataclasses import dataclass
import time import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from typing import List from typing import List
from app import instances
from app import settings from app import settings
from app.logger import logg from app.helpers import create_album_hash
from app.helpers import Get, UseBisection, create_album_hash from app.helpers import Get
from app.helpers import run_fast_scandir from app.helpers import run_fast_scandir
from app.helpers import UseBisection
from app.instances import tracks_instance from app.instances import tracks_instance
from app.lib.albumslib import create_album from app.lib.albumslib import create_album
from app.lib.taglib import get_tags from app.lib.taglib import get_tags
from app.models import Album, Track from app.logger import logg
from app.models import Album
from app.models import Track
from tqdm import tqdm from tqdm import tqdm
from app import instances
class Populate: class Populate:
""" """
@@ -76,6 +78,7 @@ class PreAlbum:
class CreateAlbums: class CreateAlbums:
def __init__(self) -> None: def __init__(self) -> None:
self.db_tracks = Get.get_all_tracks() self.db_tracks = Get.get_all_tracks()
self.db_albums = Get.get_all_albums() self.db_albums = Get.get_all_albums()
@@ -119,7 +122,8 @@ class CreateAlbums:
return prealbums return prealbums
@staticmethod @staticmethod
def filter_processed(albums: List[Album], prealbums: List[PreAlbum]) -> List[dict]: def filter_processed(albums: List[Album],
prealbums: List[PreAlbum]) -> List[dict]:
to_process = [] to_process = []
for p in tqdm(prealbums, desc="Filtering processed albums"): for p in tqdm(prealbums, desc="Filtering processed albums"):
@@ -144,7 +148,7 @@ class CreateAlbums:
album = create_album(track) album = create_album(track)
self.db_tracks.remove(track) self.db_tracks.remove(track)
else: else:
album["image"] = hash album["image"] = hash + ".webp"
try: try:
album = Album(album) album = Album(album)
return album return album
+4
View File
@@ -37,6 +37,7 @@ class Limit:
class SearchTracks: class SearchTracks:
def __init__(self, tracks: List[models.Track], query: str) -> None: def __init__(self, tracks: List[models.Track], query: str) -> None:
self.query = query self.query = query
self.tracks = tracks self.tracks = tracks
@@ -59,6 +60,7 @@ class SearchTracks:
class SearchArtists: class SearchArtists:
def __init__(self, artists: set[str], query: str) -> None: def __init__(self, artists: set[str], query: str) -> None:
self.query = query self.query = query
self.artists = artists self.artists = artists
@@ -88,6 +90,7 @@ class SearchArtists:
class SearchAlbums: class SearchAlbums:
def __init__(self, albums: List[models.Album], query: str) -> None: def __init__(self, albums: List[models.Album], query: str) -> None:
self.query = query self.query = query
self.albums = albums self.albums = albums
@@ -118,6 +121,7 @@ class SearchAlbums:
class SearchPlaylists: class SearchPlaylists:
def __init__(self, playlists: List[models.Playlist], query: str) -> None: def __init__(self, playlists: List[models.Playlist], query: str) -> None:
self.playlists = playlists self.playlists = playlists
self.query = query self.query = query
+3 -2
View File
@@ -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. Extracts the thumbnail from an audio file. Returns the path to the thumbnail.
""" """
img_path = os.path.join(settings.THUMBS_PATH, webp_path) img_path = os.path.join(settings.THUMBS_PATH, webp_path)
tsize = settings.THUMB_SIZE
if os.path.exists(img_path): if os.path.exists(img_path):
return True return True
@@ -43,12 +44,12 @@ def extract_thumb(filepath: str, webp_path: str) -> bool:
img = Image.open(BytesIO(album_art)) img = Image.open(BytesIO(album_art))
try: try:
small_img = img.resize((250, 250), Image.ANTIALIAS) small_img = img.resize((tsize, tsize), Image.ANTIALIAS)
small_img.save(img_path, format="webp") small_img.save(img_path, format="webp")
except OSError: except OSError:
try: try:
png = img.convert("RGB") 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") small_img.save(webp_path, format="webp")
except: except:
return False return False
+1 -2
View File
@@ -22,5 +22,4 @@ def validate_tracks() -> None:
def get_p_track(ptrack): def get_p_track(ptrack):
return instances.tracks_instance.find_track_by_title_artists_album( return instances.tracks_instance.find_track_by_title_artists_album(
ptrack["title"], ptrack["artists"], ptrack["album"] ptrack["title"], ptrack["artists"], ptrack["album"])
)
+5 -23
View File
@@ -4,15 +4,15 @@ This library contains the classes and functions related to the watchdog file wat
import os import os
import time import time
from app.logger import logg
from app import instances from app import instances
from app import models from app.helpers import create_album_hash
from app.helpers import Get, create_album_hash
from app.lib.albumslib import create_album
from app.lib.taglib import get_tags from app.lib.taglib import get_tags
from app.logger import get_logger
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer from watchdog.observers import Observer
log = get_logger()
class OnMyWatch: class OnMyWatch:
""" """
@@ -31,7 +31,7 @@ class OnMyWatch:
try: try:
self.observer.start() self.observer.start()
except OSError: except OSError:
logg.error("Could not start watchdog.") log.error("Could not start watchdog.")
return return
try: try:
@@ -55,18 +55,6 @@ def add_track(filepath: str) -> None:
if tags is not None: if tags is not None:
hash = create_album_hash(tags["album"], tags["albumartist"]) hash = create_album_hash(tags["album"], tags["albumartist"])
tags["albumhash"] = hash tags["albumhash"] = hash
album = instances.album_instance.find_album_by_hash(hash)
all_tracks = Get.get_all_tracks()
all_tracks.append(models.Track(tags))
if album is None:
album_data = create_album(tags, all_tracks)
album = models.Album(album_data)
instances.album_instance.insert_album(album)
tags["image"] = album.image
instances.tracks_instance.insert_song(tags) instances.tracks_instance.insert_song(tags)
@@ -75,8 +63,6 @@ def remove_track(filepath: str) -> None:
Removes a track from the music dict. Removes a track from the music dict.
""" """
filepath = filepath + "k"
instances.tracks_instance.remove_song_by_filepath(filepath) instances.tracks_instance.remove_song_by_filepath(filepath)
@@ -140,7 +126,3 @@ class Handler(PatternMatchingEventHandler):
watch = OnMyWatch() 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.
-2
View File
@@ -1,7 +1,5 @@
import logging import logging
from app.settings import logger
class CustomFormatter(logging.Formatter): class CustomFormatter(logging.Formatter):
+11 -7
View File
@@ -1,8 +1,9 @@
""" """
Contains all the models for objects generation and typing. Contains all the models for objects generation and typing.
""" """
from dataclasses import dataclass, field
import random import random
from dataclasses import dataclass
from dataclasses import field
from typing import List from typing import List
from app import helpers from app import helpers
@@ -48,16 +49,13 @@ class Track:
self.image = tags["albumhash"] + ".webp" self.image = tags["albumhash"] + ".webp"
self.tracknumber = int(tags["tracknumber"]) self.tracknumber = int(tags["tracknumber"])
self.uniq_hash = self.create_unique_hash( self.uniq_hash = self.create_unique_hash("".join(self.artists),
"".join(self.artists), self.album, self.title self.album, self.title)
)
@staticmethod @staticmethod
def create_unique_hash(*args): def create_unique_hash(*args):
ill_chars = '/\\:*?"<>|#&'
string = "".join(str(a) for a in args).replace(" ", "") string = "".join(str(a) for a in args).replace(" ", "")
return "".join(string).strip(ill_chars).lower() return "".join([i for i in string if i.isalnum()]).lower()
@dataclass(slots=True) @dataclass(slots=True)
@@ -92,6 +90,7 @@ class Album:
is_soundtrack: bool = False is_soundtrack: bool = False
is_compilation: bool = False is_compilation: bool = False
is_single: bool = False is_single: bool = False
colors: List[str] = field(default_factory=list)
def __init__(self, tags): def __init__(self, tags):
self.title = tags["title"] self.title = tags["title"]
@@ -100,6 +99,11 @@ class Album:
self.image = tags["image"] self.image = tags["image"]
self.hash = tags["hash"] self.hash = tags["hash"]
try:
self.colors = tags["colors"]
except KeyError:
self.colors = []
@property @property
def is_soundtrack(self) -> bool: def is_soundtrack(self) -> bool:
keywords = ["motion picture", "soundtrack"] keywords = ["motion picture", "soundtrack"]
+6 -10
View File
@@ -11,13 +11,11 @@ class CopyFiles:
"""Copies assets to the app directory.""" """Copies assets to the app directory."""
def __init__(self) -> None: def __init__(self) -> None:
files = [ files = [{
{ "src": "assets",
"src": "assets", "dest": os.path.join(settings.APP_DIR, "assets"),
"dest": os.path.join(settings.APP_DIR, "assets"), "is_dir": True,
"is_dir": True, }]
}
]
for entry in files: for entry in files:
src = os.path.join(os.getcwd(), entry["src"]) src = os.path.join(os.getcwd(), entry["src"])
@@ -26,9 +24,7 @@ class CopyFiles:
shutil.copytree( shutil.copytree(
src, src,
entry["dest"], entry["dest"],
ignore=shutil.ignore_patterns( ignore=shutil.ignore_patterns("*.pyc", ),
"*.pyc",
),
copy_function=shutil.copy2, copy_function=shutil.copy2,
dirs_exist_ok=True, dirs_exist_ok=True,
) )
+4 -2
View File
@@ -59,6 +59,7 @@ class Playlist:
lastUpdated: int lastUpdated: int
description: str description: str
count: int = 0 count: int = 0
duration: int = 0
def __init__(self, def __init__(self,
p: models.Playlist, p: models.Playlist,
@@ -72,7 +73,8 @@ class Playlist:
self.count = p.count self.count = p.count
if construct_last_updated: 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) return date_string_to_time_passed(date)
+6 -4
View File
@@ -1,9 +1,8 @@
""" """
Contains default configs Contains default configs
""" """
import os
import multiprocessing import multiprocessing
import os
# paths # paths
CONFIG_FOLDER = ".alice" CONFIG_FOLDER = ".alice"
@@ -28,6 +27,9 @@ LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a"
CPU_COUNT = multiprocessing.cpu_count() CPU_COUNT = multiprocessing.cpu_count()
THUMB_SIZE: int = 400
"""
The size of extracted in pixels
"""
class logger: LOGGER_ENABLE: bool = True
enable = True
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+1 -1
View File
@@ -39,7 +39,7 @@ $teal: rgb(64, 200, 224);
$primary: $gray4; $primary: $gray4;
$accent: $darkblue; $accent: $red;
$secondary: $gray5; $secondary: $gray5;
$cta: $blue; $cta: $blue;
$danger: $red; $danger: $red;
+7 -8
View File
@@ -8,10 +8,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { defineProps<{
props: ['bio'], bio: string;
}; }>();
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -48,7 +48,6 @@ export default {
width: 10rem; width: 10rem;
} }
.rect { .rect {
width: 10rem; width: 10rem;
height: 10rem; height: 10rem;
@@ -59,10 +58,10 @@ export default {
left: 7rem; left: 7rem;
transform: rotate(45deg) translate(-1rem, -9rem); transform: rotate(45deg) translate(-1rem, -9rem);
z-index: 1; z-index: 1;
transition: all .5s ease-in-out; transition: all 0.5s ease-in-out;
&:hover { &:hover {
transition: all .5s ease-in-out; transition: all 0.5s ease-in-out;
} }
} }
@@ -86,4 +85,4 @@ export default {
} }
} }
} }
</style> </style>
+140 -44
View File
@@ -1,16 +1,22 @@
<template> <template>
<div class="album-h" ref="albumheaderthing"> <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="art">
<div <img
class="image shadow-lg rounded" :src="imguri + album.image"
:style="{ alt=""
backgroundImage: `url(&quot;${imguri + album.image}&quot;)`,
}"
v-motion-slide-from-left v-motion-slide-from-left
></div> class="rounded shadow-lg"
/>
</div> </div>
<div class="info"> <div class="info" :class="{ nocontrast: isLight() }">
<div class="top" v-motion-slide-from-top> <div class="top" v-motion-slide-from-top>
<div class="h"> <div class="h">
<span v-if="album.is_soundtrack">Soundtrack</span> <span v-if="album.is_soundtrack">Soundtrack</span>
@@ -18,7 +24,9 @@
<span v-else-if="album.is_single">Single</span> <span v-else-if="album.is_single">Single</span>
<span v-else>Album</span> <span v-else>Album</span>
</div> </div>
<div class="title ellip">{{ album.title }}</div> <div class="title ellip">
{{ album.title }}
</div>
</div> </div>
<div class="bottom"> <div class="bottom">
<div class="stats"> <div class="stats">
@@ -26,7 +34,11 @@
{{ formatSeconds(album.duration, true) }} {{ album.date }} {{ formatSeconds(album.duration, true) }} {{ album.date }}
{{ album.artist }} {{ album.artist }}
</div> </div>
<PlayBtnRect :source="playSources.album" :store="useAlbumStore" /> <PlayBtnRect
:source="playSources.album"
:store="useAlbumStore"
:background="getButtonColor()"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -44,31 +56,133 @@ import { paths } from "../../config";
import { AlbumInfo } from "../../interfaces"; import { AlbumInfo } from "../../interfaces";
import PlayBtnRect from "../shared/PlayBtnRect.vue"; import PlayBtnRect from "../shared/PlayBtnRect.vue";
defineProps<{ const props = defineProps<{
album: AlbumInfo; album: AlbumInfo;
}>(); }>();
const emit = defineEmits<{
(event: "resetBottomPadding"): void;
}>();
const albumheaderthing = ref<HTMLElement>(null); const albumheaderthing = ref<HTMLElement>(null);
const imguri = paths.images.thumb; const imguri = paths.images.thumb;
const nav = useNavStore(); 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> </script>
<style lang="scss"> <style lang="scss">
.album-h {
height: 16rem;
}
.a-header { .a-header {
display: grid; display: grid;
grid-template-columns: 15rem 1fr; grid-template-columns: max-content 1fr;
gap: 1rem;
padding: 1rem; padding: 1rem;
height: 100%; height: 100%;
background-color: $black; background-color: $black;
background-color: #000000;
background-image: linear-gradient(37deg, $black 20%, $gray, $black 90%); background-image: linear-gradient(37deg, $black 20%, $gray, $black 90%);
.art { .art {
@@ -78,12 +192,17 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
.image { img {
width: 14rem; width: 15rem;
height: 14rem; height: 15rem;
transition: all 0.2s ease-in-out;
} }
} }
.nocontrast {
color: $black;
}
.info { .info {
width: 100%; width: 100%;
display: flex; display: flex;
@@ -91,20 +210,14 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
justify-content: flex-end; justify-content: flex-end;
.top { .top {
.h {
color: #ffffffcb;
}
.title { .title {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 600; font-weight: 600;
color: white;
text-transform: capitalize; text-transform: capitalize;
} }
.artist { .artist {
font-size: 1.15rem; font-size: 1.15rem;
color: #ffffffe0;
} }
} }
@@ -121,23 +234,6 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
font-size: 0.8rem; font-size: 0.8rem;
margin-bottom: 0.75rem; 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;
}
}
} }
} }
} }
+8 -52
View File
@@ -1,13 +1,6 @@
<template> <template>
<div class="folder"> <div class="folder">
<div class="table rounded" v-if="tracks.length"> <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"> <div class="songlist">
<SongItem <SongItem
v-for="track in getTracks()" v-for="track in getTracks()"
@@ -16,7 +9,7 @@
:index="track.index" :index="track.index"
@updateQueue="updateQueue" @updateQueue="updateQueue"
:isPlaying="queue.playing" :isPlaying="queue.playing"
:isCurrent="queue.current.trackid == track.trackid" :isCurrent="queue.currentid == track.trackid"
/> />
</div> </div>
</div> </div>
@@ -54,18 +47,22 @@ let route = useRoute().name;
* @param track Track object * @param track Track object
*/ */
function updateQueue(track: Track) { function updateQueue(track: Track) {
const index = props.tracks.findIndex(
(t: Track) => t.trackid === track.trackid
);
switch (route) { switch (route) {
case "FolderView": case "FolderView":
queue.playFromFolder(props.path, props.tracks); queue.playFromFolder(props.path, props.tracks);
queue.play(track); queue.play(index);
break; break;
case "AlbumView": case "AlbumView":
queue.playFromAlbum(track.album, track.albumartist, props.tracks); queue.playFromAlbum(track.album, track.albumartist, props.tracks);
queue.play(track); queue.play(index);
break; break;
case "PlaylistView": case "PlaylistView":
queue.playFromPlaylist(props.pname, props.playlistid, props.tracks); queue.playFromPlaylist(props.pname, props.playlistid, props.tracks);
queue.play(track); queue.play(index);
break; break;
} }
} }
@@ -116,47 +113,6 @@ function getTracks() {
} }
} }
.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 { .songlist {
scrollbar-width: none; scrollbar-width: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
+1 -1
View File
@@ -77,7 +77,7 @@ const menus = [
} }
svg > path { svg > path {
fill: $side-nav-svg; fill: $accent;
} }
} }
</style> </style>
+13 -8
View File
@@ -2,14 +2,19 @@
<div class="info"> <div class="info">
<div class="desc"> <div class="desc">
<div> <div>
<div class="art"> <router-link
<div :to="{
class="l-image image rounded" name: 'AlbumView',
:style="{ params: {
backgroundImage: `url(&quot;${imguri + track.image}&quot;)`, hash: track.albumhash,
}" },
></div> }"
</div> >
<div class="art">
<img :src="imguri + track.image" alt="" class="l-image rounded" />
</div>
</router-link>
<div id="bitrate"> <div id="bitrate">
<span v-if="track.bitrate > 1500">MASTER</span> <span v-if="track.bitrate > 1500">MASTER</span>
<span v-else-if="track.bitrate > 330">FLAC</span> <span v-else-if="track.bitrate > 330">FLAC</span>
+46 -15
View File
@@ -1,10 +1,16 @@
<template> <template>
<div class="l_ rounded"> <div class="l_ rounded">
<div class="headin">Now Playing</div> <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 class="separator no-border"></div>
<div> <div>
<SongCard :track="queue.current" /> <SongCard :track="queue.tracks[queue.current]" />
<Progress /> <Progress />
<HotKeys /> <HotKeys />
</div> </div>
@@ -16,8 +22,38 @@ import SongCard from "./SongCard.vue";
import HotKeys from "./NP/HotKeys.vue"; import HotKeys from "./NP/HotKeys.vue";
import Progress from "./NP/Progress.vue"; import Progress from "./NP/Progress.vue";
import useQStore from "../../stores/queue"; 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 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> </script>
<style lang="scss"> <style lang="scss">
.l_ { .l_ {
@@ -49,31 +85,26 @@ const queue = useQStore();
} }
.button { .button {
height: 2rem;
width: 2rem;
position: absolute; position: absolute;
background-size: 1.5rem;
top: $small; top: $small;
cursor: pointer; cursor: pointer;
transition: all 200ms; transition: all 200ms;
display: flex;
align-items: center;
padding: $smaller;
&:hover { &:hover {
background-color: $gray2; background-color: $accent;
} }
} }
.context_on {
background-color: $accent;
}
.menu { .menu {
right: $small; right: $small;
background-image: url("../../assets/icons/right-arrow.svg");
transform: rotate(90deg); transform: rotate(90deg);
&:hover {
transform: rotate(0deg);
}
}
br {
height: 0rem;
} }
.art { .art {
+37 -61
View File
@@ -3,81 +3,66 @@
<div class="header"> <div class="header">
<div class="headin">Featured Artists</div> <div class="headin">Featured Artists</div>
<div class="xcontrols"> <div class="xcontrols">
<div class="prev" @click="scrollLeft"></div> <div class="prev icon" @click="scrollLeft()"><ArrowSvg /></div>
<div class="next" @click="scrollRight"></div> <div class="next icon" @click="scrollRight()"><ArrowSvg /></div>
</div> </div>
</div> </div>
<div class="separator no-border"></div> <div class="separator no-border"></div>
<div class="artists" ref="artists_dom"> <div class="artists" ref="artists_dom">
<ArtistCard <ArtistCard
v-for="artist in artists" v-for="artist in artists"
:key="artist" :key="artist.image"
:artist="artist" :artist="artist"
:color="'ffffff00'" :color="'ffffff00'"
/> />
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ref } from "@vue/reactivity"; import { ref } from "@vue/reactivity";
import ArtistCard from "@/components/shared/ArtistCard.vue"; 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 { defineProps<{
props: ["artists"], artists: Artist[];
components: { }>();
ArtistCard,
},
setup() {
const artists_dom = ref(null);
const scrollLeft = () => { const artists_dom = ref(null);
const dom = artists_dom.value;
dom.scrollBy({
left: -700,
behavior: "smooth",
});
};
const scrollRight = () => { const scrollLeft = () => {
const dom = artists_dom.value; const dom = artists_dom.value;
dom.scrollBy({ dom.scrollBy({
left: 700, left: -700,
behavior: "smooth", behavior: "smooth",
}); });
}; };
return { const scrollRight = () => {
artists_dom, const dom = artists_dom.value;
scrollLeft, dom.scrollBy({
scrollRight, left: 700,
}; behavior: "smooth",
}, });
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.f-artists { .f-artists {
height: 14.5em; width: 100%;
width: calc(100%); padding: 0 $small;
padding: $small;
padding-bottom: 0;
border-radius: $small; border-radius: $small;
user-select: none; user-select: none;
background: linear-gradient(0deg, transparent, $black);
position: relative; position: relative;
background-color: #ffffff00;
.header { .header {
display: flex; display: flex;
height: 2.5rem;
align-items: center; align-items: center;
position: relative; position: relative;
.headin { .headin {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 900; font-weight: 900;
// border: solid;
margin-left: $small; margin-left: $small;
} }
} }
@@ -85,40 +70,31 @@ export default {
.f-artists .xcontrols { .f-artists .xcontrols {
z-index: 1; z-index: 1;
width: 5rem;
height: 2rem;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
display: flex; display: flex;
justify-content: space-between; gap: 1rem;
&:hover {
z-index: 1;
}
.next {
background: url(../../assets/icons/right-arrow.svg) no-repeat center;
}
.prev { .prev {
background: url(../../assets/icons/right-arrow.svg) no-repeat center;
transform: rotate(180deg); transform: rotate(180deg);
} }
.next,
.prev { .icon {
width: 2em;
height: 2em;
border-radius: $small; border-radius: $small;
cursor: pointer; cursor: pointer;
transition: all 0.5s ease; transition: all 0.5s ease;
background-color: rgb(51, 51, 51); background-color: rgb(51, 51, 51);
} padding: $smaller;
.next:hover, svg {
.prev:hover { display: flex;
background-color: $blue; }
transition: all 0.5s ease;
&:hover {
background-color: $accent;
transition: all 0.5s ease;
}
} }
} }
+12 -3
View File
@@ -12,7 +12,14 @@
<div class="carddd"> <div class="carddd">
<div class="info"> <div class="info">
<div class="btns"> <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" /> <Option @showDropdown="showDropdown" :src="context.src" />
</div> </div>
<div class="duration"> <div class="duration">
@@ -20,7 +27,8 @@
<span v-else-if="props.info.count == 1" <span v-else-if="props.info.count == 1"
>{{ props.info.count }} Track</span >{{ 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>
<div class="desc"> <div class="desc">
{{ props.info.description }} {{ props.info.description }}
@@ -51,6 +59,7 @@ import useContextStore from "../../stores/context";
import useModalStore from "../../stores/modal"; import useModalStore from "../../stores/modal";
import Option from "../shared/Option.vue"; import Option from "../shared/Option.vue";
import PlayBtnRect from "../shared/PlayBtnRect.vue"; import PlayBtnRect from "../shared/PlayBtnRect.vue";
import { formatSeconds } from "@/composables/perks";
const imguri = paths.images.playlist; const imguri = paths.images.playlist;
const context = useContextStore(); const context = useContextStore();
@@ -77,7 +86,7 @@ function showDropdown(e: any) {
.p-header { .p-header {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
height: 16rem; height: 17rem;
position: relative; position: relative;
border-radius: 0.75rem; border-radius: 0.75rem;
color: $white; color: $white;
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="r-home"> <div class="r-home">
<UpNext :next="queue.next" :playNext="queue.playNext" /> <UpNext :next="queue.tracks[queue.next]" :playNext="queue.playNext" />
<Recommendations /> <Recommendations />
</div> </div>
</template> </template>
+2 -3
View File
@@ -8,9 +8,8 @@
<div class="r-search" v-show="tabs.current === tabs.tabs.search"> <div class="r-search" v-show="tabs.current === tabs.tabs.search">
<Search /> <Search />
</div> </div>
<div class="r-queue" v-show="tabs.current === tabs.tabs.queue"> <div class="r-queue" v-show="tabs.current === tabs.tabs.queue">
<UpNext /> <Queue />
</div> </div>
</div> </div>
</div> </div>
@@ -19,7 +18,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Search from "./Search/Main.vue"; import Search from "./Search/Main.vue";
import UpNext from "./Queue.vue"; import Queue from "./Queue.vue";
import DashBoard from "./Home/Main.vue"; import DashBoard from "./Home/Main.vue";
import useTabStore from "../../stores/tabs"; import useTabStore from "../../stores/tabs";
+4 -4
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="up-next"> <div class="up-next">
<div class="r-grid"> <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="scrollable-r border rounded">
<div <div
class="inner" class="inner"
@@ -9,11 +9,11 @@
@mouseleave="setMouseOver(false)" @mouseleave="setMouseOver(false)"
> >
<TrackItem <TrackItem
v-for="t in queue.tracks" v-for="(t, index) in queue.tracks"
:key="t.trackid" :key="t.trackid"
:track="t" :track="t"
@playThis="queue.play(t)" @playThis="queue.play(index)"
:isCurrent="t.trackid === queue.current.trackid" :isCurrent="index === queue.current"
:isPlaying="queue.playing" :isPlaying="queue.playing"
/> />
</div> </div>
@@ -2,13 +2,13 @@
<div id="tracks-results" v-if="search.tracks.value"> <div id="tracks-results" v-if="search.tracks.value">
<TransitionGroup name="list"> <TransitionGroup name="list">
<TrackItem <TrackItem
v-for="track in search.tracks.value" v-for="(track, index) in search.tracks.value"
:key="track.trackid" :key="track.trackid"
:track="track" :track="track"
:isPlaying="queue.playing" :isPlaying="queue.playing"
:isCurrent="queue.current.trackid == track.trackid" :isCurrent="queue.currentid == track.trackid"
:isSearchTrack="true" :isSearchTrack="true"
@PlayThis="updateQueue" @PlayThis="updateQueue(index)"
/> />
</TransitionGroup> </TransitionGroup>
<LoadMore v-if="search.tracks.more" @loadMore="loadMore" /> <LoadMore v-if="search.tracks.more" @loadMore="loadMore" />
@@ -30,9 +30,9 @@ function loadMore() {
search.loadTracks(search.loadCounter.tracks); search.loadTracks(search.loadCounter.tracks);
} }
function updateQueue(track: Track) { function updateQueue(index: number) {
queue.playFromSearch(search.query, search.tracks.value); queue.playFromSearch(search.query, search.tracks.value);
queue.play(track); queue.play(index);
} }
</script> </script>
+2 -5
View File
@@ -57,7 +57,7 @@ const context = useContextStore();
top: 0; top: 0;
left: 0; left: 0;
width: 12rem; width: 12rem;
z-index: 10; z-index: 10000 !important;
transform: scale(0); transform: scale(0);
padding: $small; padding: $small;
@@ -68,12 +68,10 @@ const context = useContextStore();
.context-item { .context-item {
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: flex-start;
align-items: center; align-items: center;
cursor: default; cursor: default;
padding: $small; padding: $small;
border-radius: $small; border-radius: $small;
color: rgb(255, 255, 255);
position: relative; position: relative;
text-transform: capitalize; text-transform: capitalize;
@@ -141,7 +139,7 @@ const context = useContextStore();
.children { .children {
transform: scale(1); 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 { .context-menu-visible {
transform: scale(1); transform: scale(1);
transition: transform 0.2s ease-in-out;
} }
.context-normalizedX { .context-normalizedX {
+3 -3
View File
@@ -38,11 +38,11 @@ defineProps<{
cursor: pointer; cursor: pointer;
.artist-image { .artist-image {
width: 7em; width: 8em;
height: 7em; height: 8em;
border-radius: 60%; border-radius: 60%;
margin-bottom: $small; margin-bottom: $small;
background-size: 7rem 7rem; background-size: 8rem 8rem;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
transition: all 0.5s ease-in-out; transition: all 0.5s ease-in-out;
+20 -3
View File
@@ -2,8 +2,12 @@
<div <div
class="playbtnrect rounded" class="playbtnrect rounded"
@click="usePlayFrom(source, useQStore, store)" @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 class="text">Play</div>
</div> </div>
</template> </template>
@@ -11,13 +15,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { playSources } from "@/composables/enums"; import { playSources } from "@/composables/enums";
import usePlayFrom from "@/composables/usePlayFrom"; import usePlayFrom from "@/composables/usePlayFrom";
import useFStore from "@/stores/folder"; import useFStore from "@/stores/pages/folder";
import useAStore from "@/stores/pages/album"; import useAStore from "@/stores/pages/album";
import usePStore from "@/stores/pages/playlist"; import usePStore from "@/stores/pages/playlist";
import useQStore from "@/stores/queue"; import useQStore from "@/stores/queue";
import playBtnSvg from "@/assets/icons/play.svg";
defineProps<{ defineProps<{
source: playSources; source: playSources;
background?: {
color: string;
isDark?: boolean;
};
store: store:
| typeof useQStore | typeof useQStore
| typeof useFStore | typeof useFStore
@@ -34,8 +43,8 @@ defineProps<{
height: 2.5rem; height: 2.5rem;
padding-left: 0.75rem; padding-left: 0.75rem;
cursor: pointer; cursor: pointer;
background: linear-gradient(34deg, $accent, $red);
user-select: none; user-select: none;
color: $white;
transition: all 0.5s ease-in-out; transition: all 0.5s ease-in-out;
.icon { .icon {
@@ -50,4 +59,12 @@ defineProps<{
} }
} }
} }
.playbtnrectdark {
color: $black !important;
svg > path {
fill: $accent !important;
}
}
</style> </style>
+33 -26
View File
@@ -7,13 +7,12 @@
> >
<div class="index">{{ props.index }}</div> <div class="index">{{ props.index }}</div>
<div class="flex"> <div class="flex">
<div <div @click="emitUpdate(props.song)" class="thumbnail">
class="album-art image rounded" <img
:style="{ :src="imguri + props.song.image"
backgroundImage: `url(&quot;${imguri + props.song.image}&quot;`, alt=""
}" class="album-art image rounded"
@click="emitUpdate(props.song)" />
>
<div <div
class="now-playing-track image" class="now-playing-track image"
v-if="props.isPlaying && props.isCurrent" v-if="props.isPlaying && props.isCurrent"
@@ -67,17 +66,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { putCommas, formatSeconds } from "../../composables/perks"; import { putCommas, formatSeconds } from "@/composables/perks";
import useContextStore from "../../stores/context"; import useContextStore from "@/stores/context";
import useModalStore from "../../stores/modal"; import useModalStore from "@/stores/modal";
import useQueueStore from "../../stores/queue"; import useQueueStore from "@/stores/queue";
import { ContextSrc } from "../../composables/enums"; import { ContextSrc } from "@/composables/enums";
import OptionSvg from "../../assets/icons/more.svg"; import OptionSvg from "@/assets/icons/more.svg";
import { ref } from "vue"; import { ref } from "vue";
import trackContext from "../../contexts/track_context"; import trackContext from "@/contexts/track_context";
import { Track } from "../../interfaces"; import { Track } from "@/interfaces";
import { paths } from "../../config"; import { paths } from "@/config";
const contextStore = useContextStore(); const contextStore = useContextStore();
@@ -122,15 +121,14 @@ function emitUpdate(track: Track) {
.songlist-item { .songlist-item {
display: grid; display: grid;
align-items: center; align-items: center;
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 0.25fr 2.5rem; grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 2rem 2.5rem;
height: 3.75rem; height: 3.75rem;
text-align: left; text-align: left;
gap: $small; gap: $small;
user-select: none; user-select: none;
-moz-user-select: none;
@include tablet-landscape { @include tablet-landscape {
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 2.5rem; grid-template-columns: 1.5rem 1.5fr 1fr 1fr 2.5rem;
} }
@include tablet-portrait { @include tablet-portrait {
@@ -157,6 +155,10 @@ function emitUpdate(track: Track) {
max-width: max-content; max-width: max-content;
cursor: pointer; cursor: pointer;
&:hover {
text-decoration: underline;
}
@include tablet-portrait { @include tablet-portrait {
display: none; display: none;
} }
@@ -189,7 +191,7 @@ function emitUpdate(track: Track) {
font-size: 0.9rem; font-size: 0.9rem;
width: 5rem !important; width: 5rem !important;
text-align: right; text-align: left;
} }
.options-icon { .options-icon {
@@ -223,20 +225,25 @@ function emitUpdate(track: Track) {
.flex { .flex {
position: relative; position: relative;
padding-left: 4rem;
align-items: center; align-items: center;
.thumbnail {
margin-right: $small;
display: flex;
}
.album-art { .album-art {
position: absolute;
left: $small;
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
margin-right: 1rem;
display: grid;
place-items: center;
cursor: pointer; cursor: pointer;
} }
.now-playing-track {
position: absolute;
left: $small;
top: $small;
}
.title { .title {
cursor: pointer; cursor: pointer;
word-break: break-all; word-break: break-all;
+2 -2
View File
@@ -74,11 +74,11 @@ const showContextMenu = (e: Event) => {
}; };
const emit = defineEmits<{ const emit = defineEmits<{
(e: "PlayThis", track: Track): void; (e: "PlayThis"): void;
}>(); }>();
const playThis = (track: Track) => { const playThis = (track: Track) => {
emit("PlayThis", track); emit("PlayThis");
}; };
</script> </script>
+1
View File
@@ -24,6 +24,7 @@ const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
info: { info: {
album: "", album: "",
artist: "", artist: "",
colors: []
}, },
tracks: [], tracks: [],
}; };
+5 -2
View File
@@ -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) { function formatSeconds(seconds: number, long?: boolean) {
// check if there are arguments
const date = new Date(seconds * 1000); const date = new Date(seconds * 1000);
const hh = date.getUTCHours(); const hh = date.getUTCHours();
+1 -25
View File
@@ -9,29 +9,6 @@ const uris = {
artists: `${base_url}/artists?q=`, 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) { async function searchTracks(query: string) {
const url = uris.tracks + encodeURIComponent(query.trim()); const url = uris.tracks + encodeURIComponent(query.trim());
@@ -43,7 +20,6 @@ async function searchTracks(query: string) {
} }
const data = await res.json(); const data = await res.json();
console.log(data);
return data; return data;
} }
@@ -107,4 +83,4 @@ export {
}; };
// TODO: // TODO:
// Rewrite this module using `useAxios` hook // Rewrite this module using `useAxios` hook
-44
View File
@@ -2,56 +2,12 @@ import { Track, AlbumInfo, Artist } from "./../interfaces";
import { ref } from "@vue/reactivity"; import { ref } from "@vue/reactivity";
import { reactive } from "vue"; import { reactive } from "vue";
const search_query = ref("");
const queue = ref(
Array<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(<Track>{
title: "Nothing played yet",
artists: ["... blah blah blah"],
image: "http://127.0.0.1:8900/images/thumbnails/4.webp",
trackid: "",
});
const prev = ref(<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<Track>(),
info: <AlbumInfo>{},
artists: Array<Artist>(),
bio: "",
});
const loading = ref(false); const loading = ref(false);
const is_playing = ref(false);
const settings = reactive({ const settings = reactive({
uri: "http://127.0.0.1:9876", uri: "http://127.0.0.1:9876",
}); });
export default { export default {
search_query,
queue,
folder_song_list,
folder_list,
current,
prev,
loading, loading,
is_playing,
album,
settings, settings,
}; };
+3 -3
View File
@@ -25,14 +25,14 @@ export default function play(
const f = store(); const f = store();
useQueue.playFromFolder(f.path, f.tracks); useQueue.playFromFolder(f.path, f.tracks);
useQueue.play(f.tracks[0]); useQueue.play();
break; break;
case playSources.album: case playSources.album:
store = store as typeof album; store = store as typeof album;
const a = store(); const a = store();
useQueue.playFromAlbum(a.info.title, a.info.artist, a.tracks); useQueue.playFromAlbum(a.info.title, a.info.artist, a.tracks);
useQueue.play(store().tracks[0]); useQueue.play();
break; break;
case playSources.playlist: case playSources.playlist:
store = store as typeof playlist; store = store as typeof playlist;
@@ -41,7 +41,7 @@ export default function play(
if (p.tracks.length === 0) return; if (p.tracks.length === 0) return;
useQueue.playFromPlaylist(p.info.name, p.info.playlistid, p.tracks); useQueue.playFromPlaylist(p.info.name, p.info.playlistid, p.tracks);
useQueue.play(store().tracks[0]); useQueue.play();
break; break;
} }
} }
+5 -2
View File
@@ -15,7 +15,7 @@ export interface Track {
image: string; image: string;
tracknumber?: number; tracknumber?: number;
disknumber?: number; disknumber?: number;
index?: number index?: number;
} }
export interface Folder { export interface Folder {
@@ -27,6 +27,7 @@ export interface Folder {
} }
export interface AlbumInfo { export interface AlbumInfo {
albumid: string;
title: string; title: string;
artist: string; artist: string;
count: number; count: number;
@@ -36,7 +37,8 @@ export interface AlbumInfo {
is_compilation: boolean; is_compilation: boolean;
is_soundtrack: boolean; is_soundtrack: boolean;
is_single: boolean; is_single: boolean;
hash: string hash: string;
colors: string[];
} }
export interface Artist { export interface Artist {
@@ -62,6 +64,7 @@ export interface Playlist {
count?: number; count?: number;
lastUpdated?: string; lastUpdated?: string;
thumb?: string; thumb?: string;
duration?: number
} }
export interface Notif { export interface Notif {
+10 -13
View File
@@ -1,20 +1,17 @@
import { createRouter, createWebHashHistory } from "vue-router"; import state from "@/composables/state";
import Home from "@/views/Home.vue"; import useAStore from "@/stores/pages/album";
import FolderView from "@/views/FolderView.vue"; import useFStore from "@/stores/pages/folder";
import PlaylistView from "@/views/PlaylistView.vue"; import usePTrackStore from "@/stores/pages/playlist";
import Playlists from "@/views/Playlists.vue"; import usePStore from "@/stores/pages/playlists";
import AlbumsExplorer from "@/views/AlbumsExplorer.vue"; import AlbumsExplorer from "@/views/AlbumsExplorer.vue";
import AlbumView from "@/views/AlbumView.vue"; import AlbumView from "@/views/AlbumView.vue";
import ArtistsExplorer from "@/views/ArtistsExplorer.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 SettingsView from "@/views/SettingsView.vue";
import { createRouter, createWebHashHistory } from "vue-router";
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";
const routes = [ const routes = [
{ {
+33 -42
View File
@@ -25,20 +25,22 @@ function writeQueue(
); );
} }
function writeCurrent(track: Track) { function writeCurrent(index: number) {
localStorage.setItem("current", JSON.stringify(track)); localStorage.setItem("current", JSON.stringify(index));
} }
function readCurrent(): Track { function readCurrent(): number {
const current = localStorage.getItem("current"); const current = localStorage.getItem("current");
if (current) { if (current) {
return JSON.parse(current); return JSON.parse(current);
} }
return defaultTrack; return 0;
} }
const defaultTrack = <Track>{ const defaultTrack = <Track>{
title: "Nothing played yet", title: "Nothing played yet",
albumhash: " ",
artists: ["Alice"], artists: ["Alice"],
trackid: "", trackid: "",
image: "", image: "",
@@ -52,18 +54,22 @@ export default defineStore("Queue", {
current_time: 0, current_time: 0,
duration: 0, duration: 0,
}, },
current: <Track>{}, current: 0,
next: <Track>{}, next: 0,
prev: <Track>{}, prev: 0,
currentid: "",
playing: false, playing: false,
from: <fromFolder>{} || <fromAlbum>{} || <fromPlaylist>{}, from: <fromFolder>{} || <fromAlbum>{} || <fromPlaylist>{},
tracks: <Track[]>[defaultTrack], tracks: <Track[]>[defaultTrack],
}), }),
actions: { actions: {
play(track: Track) { play(index: number = 0) {
const track = this.tracks[index];
this.current = index;
this.currentid = track.trackid;
const uri = state.settings.uri + "/file/" + track.trackid; const uri = state.settings.uri + "/file/" + track.trackid;
const elem = document.getElementById("progress"); const elem = document.getElementById("progress");
this.updateCurrent(track); this.updateCurrent(index);
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
this.audio.autoplay = true; this.audio.autoplay = true;
@@ -130,39 +136,27 @@ export default defineStore("Queue", {
this.updateCurrent(readCurrent()); this.updateCurrent(readCurrent());
}, },
updateCurrent(track: Track) { updateCurrent(index: number) {
this.current = track; this.updateNext(index);
this.updatePrev(index);
this.updateNext(this.current); writeCurrent(index);
this.updatePrev(this.current);
writeCurrent(track);
}, },
updateNext(track: Track) { updateNext(index: number) {
const index = this.tracks.findIndex(
(t: Track) => t.trackid == track.trackid
);
if (index == this.tracks.length - 1) { if (index == this.tracks.length - 1) {
this.next = this.tracks[0]; this.next = 0;
} else if (index == 0) { return;
this.next = this.tracks[1];
} else {
this.next = this.tracks[index + 1];
} }
},
updatePrev(track: Track) {
const index = this.tracks.findIndex(
(t: Track) => t.trackid === track.trackid
);
this.next = index + 1;
},
updatePrev(index: number) {
if (index === 0) { if (index === 0) {
this.prev = this.tracks[this.tracks.length - 1]; this.prev = this.tracks.length - 1;
} else if (index === this.tracks.length - 1) { return;
this.prev = this.tracks[index - 1];
} else {
this.prev = this.tracks[index - 1];
} }
this.prev = index - 1;
}, },
setNewQueue(tracklist: Track[]) { setNewQueue(tracklist: Track[]) {
if (this.tracks !== tracklist) { if (this.tracks !== tracklist) {
@@ -212,14 +206,11 @@ export default defineStore("Queue", {
}, },
playTrackNext(track: Track) { playTrackNext(track: Track) {
const Toast = useNotifStore(); const Toast = useNotifStore();
const currentid = this.tracks.findIndex( if (this.current == this.tracks.length - 1) {
(t: Track) => t.trackid === this.current.trackid
);
if (currentid == this.tracks.length - 1) {
this.tracks.push(track); this.tracks.push(track);
} else { } else {
const next: Track = this.tracks[currentid + 1]; const nextindex = this.current + 1;
const next: Track = this.tracks[nextindex];
if (next.trackid === track.trackid) { if (next.trackid === track.trackid) {
Toast.showNotification("Track is already queued", NotifType.Info); Toast.showNotification("Track is already queued", NotifType.Info);
@@ -227,7 +218,7 @@ export default defineStore("Queue", {
} }
} }
this.tracks.splice(currentid + 1, 0, track); this.tracks.splice(this.current + 1, 0, track);
this.updateNext(this.current); this.updateNext(this.current);
Toast.showNotification( Toast.showNotification(
`Added ${track.title} to queue`, `Added ${track.title} to queue`,
+1 -1
View File
@@ -3,8 +3,8 @@ import { focusElem } from "../composables/perks";
const tablist = { const tablist = {
home: "home", home: "home",
search: "search",
queue: "queue", queue: "queue",
search: "search",
}; };
export default defineStore("tabs", { export default defineStore("tabs", {
+143 -14
View File
@@ -1,17 +1,35 @@
<template> <template>
<div class="al-view rounded"> <div class="al-view rounded">
<div> <div class="al-content" id="albumcontent">
<Header :album="album.info" /> <div>
</div> <Header :album="album.info" @resetBottomPadding="resetBottomPadding" />
<div class="separator" id="av-sep"></div> </div>
<div class="songs rounded"> <div class="songs rounded">
<SongList :tracks="album.tracks" :on_album_page="true"/> <SongList :tracks="album.tracks" :on_album_page="true" />
</div> </div>
<div class="separator" id="av-sep"></div> <div
<FeaturedArtists :artists="album.artists" /> id="bottom-items"
<div v-if="album.bio"> class="rounded"
<div class="separator" id="av-sep"></div> ref="albumbottomcards"
<AlbumBio :bio="album.bio" /> :class="{
bottomexpanded: bottomContainerRaised,
}"
>
<div class="click-to-expand" @click="toggleBottom">
<div>
<div class="arrow"></div>
<span>tap here</span>
</div>
</div>
<div class="bottom-content">
<FeaturedArtists :artists="album.artists" />
<div v-if="album.bio">
<div class="separator" id="av-sep"></div>
<AlbumBio :bio="album.bio" />
</div>
<div class="dummy"></div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -22,25 +40,76 @@ import AlbumBio from "../components/AlbumView/AlbumBio.vue";
import SongList from "../components/FolderView/SongList.vue"; import SongList from "../components/FolderView/SongList.vue";
import FeaturedArtists from "../components/PlaylistView/FeaturedArtists.vue"; import FeaturedArtists from "../components/PlaylistView/FeaturedArtists.vue";
import useAStore from "../stores/pages/album"; import useAStore from "../stores/pages/album";
import { onBeforeRouteUpdate } from "vue-router"; import { onBeforeRouteUpdate } from "vue-router";
import { onMounted, ref } from "vue";
const album = useAStore(); const album = useAStore();
const albumbottomcards = ref<HTMLElement>(null);
const bottomContainerRaised = ref(false);
let elem: HTMLElement = null;
let classlist: DOMTokenList = null;
onMounted(() => {
elem = document.getElementById("albumcontent");
classlist = elem.classList;
});
onBeforeRouteUpdate(async (to) => { onBeforeRouteUpdate(async (to) => {
await album.fetchTracksAndArtists(to.params.hash.toString()); await album.fetchTracksAndArtists(to.params.hash.toString());
album.fetchBio(to.params.hash.toString()); album.fetchBio(to.params.hash.toString());
}); });
/**
* Toggles the state of the bottom container. Adds the `addbottompadding` class that adds a bottom padding to the album content div.
*/
function toggleBottom() {
bottomContainerRaised.value = !bottomContainerRaised.value;
if (bottomContainerRaised.value) {
classlist.add("addbottompadding");
return;
}
if (elem.scrollTop == 0) {
classlist.remove("addbottompadding");
}
}
/**
* Called when the album page header gets into the viewport.
* Removes the bottom padding which was added when you expand the bottom container.
*/
function resetBottomPadding() {
if (bottomContainerRaised.value) return;
classlist.remove("addbottompadding");
}
</script> </script>
<style lang="scss"> <style lang="scss">
.al-view { .al-view {
scrollbar-width: none;
height: 100%; height: 100%;
position: relative;
margin-right: -$small;
overflow: hidden;
.al-content {
height: 100%;
overflow: auto;
padding-bottom: 17rem;
padding-right: $small;
transition: all 0.5s;
z-index: -1 !important;
}
.addbottompadding {
padding-bottom: 37rem;
}
.songs { .songs {
min-height: calc(100% - 31.5rem); min-height: calc(100% - 31.5rem);
margin-top: $small;
} }
&::-webkit-scrollbar { &::-webkit-scrollbar {
@@ -50,5 +119,65 @@ onBeforeRouteUpdate(async (to) => {
#av-sep { #av-sep {
border: none; border: none;
} }
#bottom-items {
position: absolute;
bottom: 0;
width: calc(100% - $small);
height: 15rem;
background-color: $gray;
transition: all 0.5s ease;
overscroll-behavior: contain;
display: grid;
grid-template-rows: 2rem 1fr;
.bottom-content {
overflow: hidden;
scroll-behavior: contain;
}
.click-to-expand {
height: 1.5rem;
display: flex;
align-items: center;
color: $gray1;
div {
margin: 0 auto;
font-size: small;
cursor: default;
user-select: none;
display: flex;
gap: $small;
}
.arrow {
max-width: min-content;
transition: all 0.2s ease-in-out;
}
&:hover {
color: $accent !important;
}
}
}
.bottomexpanded {
height: 32rem !important;
scroll-behavior: contain;
.arrow {
transform: rotate(180deg) !important;
}
.bottom-content {
overflow: auto !important;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
}
} }
</style> </style>
+2 -2
View File
@@ -7,7 +7,7 @@
- [ ] Add and deploy demo branch - [ ] Add and deploy demo branch
### Client ### Client
- [ ] Add processes tab to show running tasks, eg. when tagging files. I have no idea on how to go about it so far. Web sockets? - [ ] Add processes tab to show running tasks, eg. when tagging files. I have no idea on how to go about it so far. Web sockets?
- [ ] Responsiveness, especially the track list. - [ ] Responsiveness, especially the track list.
- [ ] Make dummy buttons functional. - [ ] Make dummy buttons functional.
- [ ] Add settings page (or modal) - [ ] Add settings page (or modal)
@@ -21,4 +21,4 @@
- Resolve album page using albumhash instead of album title and artist - Resolve album page using albumhash instead of album title and artist
+2 -2
View File
@@ -1,6 +1,6 @@
import { defineConfig } from "vite";
import svgLoader from 'vite-svg-loader'
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import svgLoader from "vite-svg-loader";
const path = require("path"); const path = require("path");