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,
checking and creating config dirs and starting the re-indexing process using a background thread.
"""
from app import functions
from app import helpers
from app import prep
+20 -11
View File
@@ -6,12 +6,12 @@ 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 flask import Blueprint
from flask import request
from app.functions import FetchAlbumBio
from app import instances
album_bp = Blueprint("album", __name__, url_prefix="")
@@ -41,27 +41,31 @@ 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."}
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 = helpers.RemoveDuplicates(tracks)()
album = instances.album_instance.find_album_by_hash(albumhash)
if not album:
return {"error": "Album not created yet."}, 204
return error_msg, 204
album = models.Album(album)
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 (
album.count == 1
and tracks[0].title == album.title
and tracks[0].tracknumber == 1
and tracks[0].disknumber == 1
):
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 {"tracks": tracks, "info": album}
@@ -72,12 +76,17 @@ def get_album_bio():
"""Returns the album bio for the given album."""
data = request.get_json()
album_hash = data["hash"]
err_msg = {"bio": "No bio found"}
album = instances.album_instance.find_album_by_hash(album_hash)
if album is None:
return err_msg, 404
bio = FetchAlbumBio(album["title"], album["artist"])()
if bio is None:
return {"bio": "No bio found."}, 404
return err_msg, 404
return {"bio": bio}
-1
View File
@@ -10,7 +10,6 @@ from flask import Blueprint
artist_bp = Blueprint("artist", __name__, url_prefix="/")
# @artist_bp.route("/artist/<artist>")
# @cache.cached()
# 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 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 Get, UseBisection, create_new_date
playlist_bp = Blueprint("playlist", __name__, url_prefix="/")
PlaylistExists = exceptions.PlaylistExists
@@ -27,7 +28,8 @@ def get_all_playlists():
dbplaylists = [models.Playlist(p) for p in dbplaylists]
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(
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)
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"])
+6 -6
View File
@@ -3,14 +3,14 @@ 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
from app import models
from app import serializer
search_bp = Blueprint("search", __name__, url_prefix="/")
SEARCH_RESULTS = {
@@ -197,20 +197,20 @@ def search_load_more():
if type == "tracks":
t = SearchResults.tracks
return {
"tracks": t[index : index + 5],
"tracks": t[index:index + 5],
"more": len(t) > index + 5,
}
elif type == "albums":
a = SearchResults.albums
return {
"albums": a[index : index + 6],
"albums": a[index:index + 6],
"more": len(a) > index + 6,
}
elif type == "artists":
a = SearchResults.artists
return {
"artists": a[index : index + 6],
"artists": a[index: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 instances
from app import models
from flask import Blueprint
from flask import send_file
from app import models
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.
"""
track = instances.tracks_instance.get_track_by_id(trackid)
msg = {"msg": "File Not Found"}
if track is None:
return "File not found", 404
return msg, 404
track = models.Track(track)
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")
@@ -33,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")
+20 -2
View File
@@ -2,6 +2,8 @@
This file contains the Album class for interacting with
album documents in MongoDB.
"""
from typing import List
from app.db.mongodb import convert_many
from app.db.mongodb import convert_one
from app.db.mongodb import MongoAlbums
@@ -20,8 +22,13 @@ class Albums(MongoAlbums):
"""
album = album.__dict__
return self.collection.update_one(
{"album": album["title"], "artist": album["artist"]},
{"$set": album},
{
"album": album["title"],
"artist": album["artist"]
},
{
"$set": album
},
upsert=True,
).upserted_id
@@ -59,3 +66,14 @@ class Albums(MongoAlbums):
"""
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
}},
)
+13 -5
View File
@@ -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 import helpers
from bson import ObjectId
@@ -18,8 +18,12 @@ class Playlists(MongoPlaylists):
Inserts a new playlist object into the database.
"""
return self.collection.update_one(
{"name": playlist["name"]},
{"$set": playlist},
{
"name": playlist["name"]
},
{
"$set": playlist
},
upsert=True,
).upserted_id
@@ -45,7 +49,9 @@ class Playlists(MongoPlaylists):
return self.collection.update_one(
{"_id": ObjectId(playlistid)},
{"$set": {"lastUpdated": date}},
{"$set": {
"lastUpdated": date
}},
)
def add_track_to_playlist(self, playlistid: str, track: dict) -> None:
@@ -56,7 +62,9 @@ class Playlists(MongoPlaylists):
{
"_id": ObjectId(playlistid),
},
{"$push": {"pre_tracks": track}},
{"$push": {
"pre_tracks": track
}},
)
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.
"""
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
@@ -18,9 +20,12 @@ 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):
"""
@@ -52,21 +57,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:
@@ -80,7 +97,9 @@ 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:
@@ -102,8 +121,10 @@ class Tracks(MongoTracks):
Returns a list of all the tracks matching the path in the query params.
"""
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:
"""
@@ -117,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:
@@ -155,13 +178,14 @@ class Tracks(MongoTracks):
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:
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}
)
song = self.collection.find_one({
"title": title,
"artists": artist,
"album": album
})
return convert_one(song)
+43 -30
View File
@@ -7,14 +7,19 @@ from concurrent.futures import ThreadPoolExecutor
from io import BytesIO
import requests
from PIL import Image
from app import helpers, settings
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 import trackslib
from app.lib.populate import CreateAlbums, Populate
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
log = get_logger()
@helpers.background
@@ -22,18 +27,25 @@ def run_checks():
"""
Checks for new songs every 5 minutes.
"""
# while True:
trackslib.validate_tracks()
Populate()
CreateAlbums()
if helpers.Ping()():
CheckArtistImages()()
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
@@ -67,6 +79,7 @@ class getArtistImage:
class useImageDownloader:
def __init__(self, url: str, dest: str) -> None:
self.url = url
self.dest = dest
@@ -76,14 +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:
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):
@@ -104,29 +121,25 @@ class CheckArtistImages:
:param artistname: The artist name
"""
img_path = (
settings.APP_DIR
+ "/images/artists/"
+ helpers.create_safe_name(artistname)
+ ".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
useImageDownloader(url, img_path)()
return "url is none"
return useImageDownloader(url, img_path)()
def __call__(self):
self.artists = helpers.Get.get_all_artists()
with ThreadPoolExecutor() as pool:
iter = pool.map(self.download_image, self.artists)
for i in iter:
pass
[i for i in iter]
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.
"""
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)
@@ -146,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
+9 -14
View File
@@ -1,19 +1,16 @@
"""
This module contains mini functions for the server.
"""
from dataclasses import dataclass
import os
from pprint import pprint
import threading
from datetime import datetime
from typing import Dict, Set
from typing import Dict
from typing import List
from typing import Set
import requests
from app import models
from app import instances
from app.lib.albumslib import Thumbnail
from app import models
def background(func):
@@ -53,9 +50,8 @@ def run_fast_scandir(__dir: str, full=False) -> Dict[List[str], List[str]]:
return subfolders, files
class RemoveDuplicates:
def __init__(self, tracklist: List[models.Track]) -> None:
self.tracklist = tracklist
@@ -77,15 +73,12 @@ def is_valid_file(filename: str) -> bool:
return False
ill_chars = '/\\:*?"<>|#&'
def create_album_hash(title: str, artist: str) -> str:
"""
Creates a simple hash for an album
"""
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
@@ -99,7 +92,7 @@ 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 ill_chars])
return "".join([i for i in name if i.isalnum()])
class UseBisection:
@@ -110,7 +103,8 @@ class UseBisection:
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.queries_list = queries
self.attr = search_from
@@ -140,6 +134,7 @@ class UseBisection:
class Get:
@staticmethod
def get_all_tracks() -> List[models.Track]:
"""
+7 -15
View File
@@ -6,7 +6,9 @@ import random
from dataclasses import dataclass
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.logger import logg
from app.settings import THUMBS_PATH
@@ -35,6 +37,7 @@ class RipAlbumImage:
class ValidateAlbumThumbs:
@staticmethod
def remove_obsolete():
"""
@@ -58,7 +61,9 @@ class ValidateAlbumThumbs:
Re-rip lost album thumbnails
"""
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()
thumbs = [(album.hash + ".webp") for album in albums]
@@ -85,19 +90,6 @@ class ValidateAlbumThumbs:
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:
"""
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
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
+12 -9
View File
@@ -1,13 +1,12 @@
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from os import scandir
import time
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.models import Folder
from app.models import Track
@dataclass
@@ -27,10 +26,14 @@ def get_folder_track_count(path: str) -> int:
def create_folder(dir: Dir) -> Folder:
"""Create a single Folder object"""
folder = {
"name": dir.path.split("/")[-1],
"path": dir.path,
"is_sym": dir.is_sym,
"trackcount": instances.tracks_instance.find_tracks_inside_path_regex(dir.path),
"name":
dir.path.split("/")[-1],
"path":
dir.path,
"is_sym":
dir.is_sym,
"trackcount":
instances.tracks_instance.find_tracks_inside_path_regex(dir.path),
}
return Folder(folder)
+9 -7
View File
@@ -11,14 +11,13 @@ 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 werkzeug import datastructures
from app.lib import trackslib
from app.helpers import Get
from app.logger import get_logger
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
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
"""
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
@@ -71,11 +71,13 @@ def save_p_image(file: datastructures.FileStorage, pid: str):
"""
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"
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":
frames = []
+12 -8
View File
@@ -1,20 +1,22 @@
from dataclasses import dataclass
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from typing import List
from app import instances
from app import settings
from app.logger import logg
from app.helpers import Get, UseBisection, create_album_hash
from app.helpers import create_album_hash
from app.helpers import Get
from app.helpers import run_fast_scandir
from app.helpers import UseBisection
from app.instances import tracks_instance
from app.lib.albumslib import create_album
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 app import instances
class Populate:
"""
@@ -76,6 +78,7 @@ class PreAlbum:
class CreateAlbums:
def __init__(self) -> None:
self.db_tracks = Get.get_all_tracks()
self.db_albums = Get.get_all_albums()
@@ -119,7 +122,8 @@ class CreateAlbums:
return prealbums
@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 = []
for p in tqdm(prealbums, desc="Filtering processed albums"):
@@ -144,7 +148,7 @@ class CreateAlbums:
album = create_album(track)
self.db_tracks.remove(track)
else:
album["image"] = hash
album["image"] = hash + ".webp"
try:
album = Album(album)
return album
+4
View File
@@ -37,6 +37,7 @@ class Limit:
class SearchTracks:
def __init__(self, tracks: List[models.Track], query: str) -> None:
self.query = query
self.tracks = tracks
@@ -59,6 +60,7 @@ class SearchTracks:
class SearchArtists:
def __init__(self, artists: set[str], query: str) -> None:
self.query = query
self.artists = artists
@@ -88,6 +90,7 @@ class SearchArtists:
class SearchAlbums:
def __init__(self, albums: List[models.Album], query: str) -> None:
self.query = query
self.albums = albums
@@ -118,6 +121,7 @@ class SearchAlbums:
class SearchPlaylists:
def __init__(self, playlists: List[models.Playlist], query: str) -> None:
self.playlists = playlists
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.
"""
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
+1 -2
View File
@@ -22,5 +22,4 @@ def validate_tracks() -> None:
def get_p_track(ptrack):
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 time
from app.logger import logg
from app import instances
from app import models
from app.helpers import Get, create_album_hash
from app.lib.albumslib import create_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:
"""
@@ -31,7 +31,7 @@ class OnMyWatch:
try:
self.observer.start()
except OSError:
logg.error("Could not start watchdog.")
log.error("Could not start watchdog.")
return
try:
@@ -55,18 +55,6 @@ def add_track(filepath: str) -> None:
if tags is not None:
hash = create_album_hash(tags["album"], tags["albumartist"])
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)
@@ -75,8 +63,6 @@ def remove_track(filepath: str) -> None:
Removes a track from the music dict.
"""
filepath = filepath + "k"
instances.tracks_instance.remove_song_by_filepath(filepath)
@@ -140,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.
-2
View File
@@ -1,7 +1,5 @@
import logging
from app.settings import logger
class CustomFormatter(logging.Formatter):
+11 -7
View File
@@ -1,8 +1,9 @@
"""
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 helpers
@@ -48,16 +49,13 @@ class Track:
self.image = tags["albumhash"] + ".webp"
self.tracknumber = int(tags["tracknumber"])
self.uniq_hash = self.create_unique_hash(
"".join(self.artists), self.album, self.title
)
self.uniq_hash = self.create_unique_hash("".join(self.artists),
self.album, self.title)
@staticmethod
def create_unique_hash(*args):
ill_chars = '/\\:*?"<>|#&'
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)
@@ -92,6 +90,7 @@ class Album:
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"]
@@ -100,6 +99,11 @@ class Album:
self.image = tags["image"]
self.hash = tags["hash"]
try:
self.colors = tags["colors"]
except KeyError:
self.colors = []
@property
def is_soundtrack(self) -> bool:
keywords = ["motion picture", "soundtrack"]
+6 -10
View File
@@ -11,13 +11,11 @@ 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,
}
]
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"])
@@ -26,9 +24,7 @@ class CopyFiles:
shutil.copytree(
src,
entry["dest"],
ignore=shutil.ignore_patterns(
"*.pyc",
),
ignore=shutil.ignore_patterns("*.pyc", ),
copy_function=shutil.copy2,
dirs_exist_ok=True,
)
+4 -2
View File
@@ -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)
+6 -4
View File
@@ -1,9 +1,8 @@
"""
Contains default configs
"""
import os
import multiprocessing
import os
# paths
CONFIG_FOLDER = ".alice"
@@ -28,6 +27,9 @@ LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a"
CPU_COUNT = multiprocessing.cpu_count()
THUMB_SIZE: int = 400
"""
The size of extracted in pixels
"""
class logger:
enable = True
LOGGER_ENABLE: bool = 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;
$accent: $darkblue;
$accent: $red;
$secondary: $gray5;
$cta: $blue;
$danger: $red;
+7 -8
View File
@@ -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>
+140 -44
View File
@@ -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(&quot;${imguri + album.image}&quot;)`,
}"
<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;
}
}
}
}
}
+8 -52
View File
@@ -1,13 +1,6 @@
<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 in getTracks()"
@@ -16,7 +9,7 @@
:index="track.index"
@updateQueue="updateQueue"
:isPlaying="queue.playing"
:isCurrent="queue.current.trackid == track.trackid"
:isCurrent="queue.currentid == track.trackid"
/>
</div>
</div>
@@ -54,18 +47,22 @@ 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;
}
}
@@ -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 {
scrollbar-width: none;
&::-webkit-scrollbar {
+1 -1
View File
@@ -77,7 +77,7 @@ const menus = [
}
svg > path {
fill: $side-nav-svg;
fill: $accent;
}
}
</style>
+13 -8
View File
@@ -2,14 +2,19 @@
<div class="info">
<div class="desc">
<div>
<div class="art">
<div
class="l-image image rounded"
:style="{
backgroundImage: `url(&quot;${imguri + track.image}&quot;)`,
}"
></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>
+46 -15
View File
@@ -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 {
+37 -61
View File
@@ -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 -3
View File
@@ -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 -1
View File
@@ -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>
+2 -3
View File
@@ -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";
+4 -4
View File
@@ -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>
@@ -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" />
@@ -30,9 +30,9 @@ function loadMore() {
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>
+2 -5
View File
@@ -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 {
+3 -3
View File
@@ -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;
+20 -3
View File
@@ -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>
+33 -26
View File
@@ -7,13 +7,12 @@
>
<div class="index">{{ props.index }}</div>
<div class="flex">
<div
class="album-art image rounded"
:style="{
backgroundImage: `url(&quot;${imguri + props.song.image}&quot;`,
}"
@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"
@@ -67,17 +66,17 @@
</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 OptionSvg from "../../assets/icons/more.svg";
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();
@@ -122,15 +121,14 @@ function emitUpdate(track: Track) {
.songlist-item {
display: grid;
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;
text-align: left;
gap: $small;
user-select: none;
-moz-user-select: none;
@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 {
@@ -157,6 +155,10 @@ function emitUpdate(track: Track) {
max-width: max-content;
cursor: pointer;
&:hover {
text-decoration: underline;
}
@include tablet-portrait {
display: none;
}
@@ -189,7 +191,7 @@ function emitUpdate(track: Track) {
font-size: 0.9rem;
width: 5rem !important;
text-align: right;
text-align: left;
}
.options-icon {
@@ -223,20 +225,25 @@ function emitUpdate(track: Track) {
.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;
+2 -2
View File
@@ -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>
+1
View File
@@ -24,6 +24,7 @@ const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
info: {
album: "",
artist: "",
colors: []
},
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) {
// check if there are arguments
const date = new Date(seconds * 1000);
const hh = date.getUTCHours();
+1 -25
View File
@@ -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());
@@ -43,7 +20,6 @@ async function searchTracks(query: string) {
}
const data = await res.json();
console.log(data);
return data;
}
@@ -107,4 +83,4 @@ export {
};
// 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 { 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 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,
};
+3 -3
View File
@@ -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;
}
}
+5 -2
View File
@@ -15,7 +15,7 @@ export interface Track {
image: string;
tracknumber?: number;
disknumber?: number;
index?: number
index?: number;
}
export interface Folder {
@@ -27,6 +27,7 @@ export interface Folder {
}
export interface AlbumInfo {
albumid: string;
title: string;
artist: string;
count: number;
@@ -36,7 +37,8 @@ export interface AlbumInfo {
is_compilation: boolean;
is_soundtrack: boolean;
is_single: boolean;
hash: string
hash: string;
colors: string[];
}
export interface Artist {
@@ -62,6 +64,7 @@ export interface Playlist {
count?: number;
lastUpdated?: string;
thumb?: string;
duration?: number
}
export interface Notif {
+10 -13
View File
@@ -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 = [
{
+33 -42
View File
@@ -25,20 +25,22 @@ function writeQueue(
);
}
function writeCurrent(track: Track) {
localStorage.setItem("current", JSON.stringify(track));
function writeCurrent(index: number) {
localStorage.setItem("current", JSON.stringify(index));
}
function readCurrent(): Track {
function readCurrent(): number {
const current = localStorage.getItem("current");
if (current) {
return JSON.parse(current);
}
return defaultTrack;
return 0;
}
const defaultTrack = <Track>{
title: "Nothing played yet",
albumhash: " ",
artists: ["Alice"],
trackid: "",
image: "",
@@ -52,18 +54,22 @@ export default defineStore("Queue", {
current_time: 0,
duration: 0,
},
current: <Track>{},
next: <Track>{},
prev: <Track>{},
current: 0,
next: 0,
prev: 0,
currentid: "",
playing: false,
from: <fromFolder>{} || <fromAlbum>{} || <fromPlaylist>{},
tracks: <Track[]>[defaultTrack],
}),
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 elem = document.getElementById("progress");
this.updateCurrent(track);
this.updateCurrent(index);
new Promise((resolve, reject) => {
this.audio.autoplay = true;
@@ -130,39 +136,27 @@ export default defineStore("Queue", {
this.updateCurrent(readCurrent());
},
updateCurrent(track: Track) {
this.current = track;
updateCurrent(index: number) {
this.updateNext(index);
this.updatePrev(index);
this.updateNext(this.current);
this.updatePrev(this.current);
writeCurrent(track);
writeCurrent(index);
},
updateNext(track: Track) {
const index = this.tracks.findIndex(
(t: Track) => t.trackid == track.trackid
);
updateNext(index: number) {
if (index == this.tracks.length - 1) {
this.next = this.tracks[0];
} else if (index == 0) {
this.next = this.tracks[1];
} else {
this.next = this.tracks[index + 1];
this.next = 0;
return;
}
},
updatePrev(track: Track) {
const index = this.tracks.findIndex(
(t: Track) => t.trackid === track.trackid
);
this.next = index + 1;
},
updatePrev(index: number) {
if (index === 0) {
this.prev = this.tracks[this.tracks.length - 1];
} else if (index === this.tracks.length - 1) {
this.prev = this.tracks[index - 1];
} else {
this.prev = this.tracks[index - 1];
this.prev = this.tracks.length - 1;
return;
}
this.prev = index - 1;
},
setNewQueue(tracklist: Track[]) {
if (this.tracks !== tracklist) {
@@ -212,14 +206,11 @@ export default defineStore("Queue", {
},
playTrackNext(track: Track) {
const Toast = useNotifStore();
const currentid = this.tracks.findIndex(
(t: Track) => t.trackid === this.current.trackid
);
if (currentid == this.tracks.length - 1) {
if (this.current == this.tracks.length - 1) {
this.tracks.push(track);
} else {
const next: Track = this.tracks[currentid + 1];
const nextindex = this.current + 1;
const next: Track = this.tracks[nextindex];
if (next.trackid === track.trackid) {
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);
Toast.showNotification(
`Added ${track.title} to queue`,
+1 -1
View File
@@ -3,8 +3,8 @@ import { focusElem } from "../composables/perks";
const tablist = {
home: "home",
search: "search",
queue: "queue",
search: "search",
};
export default defineStore("tabs", {
+143 -14
View File
@@ -1,17 +1,35 @@
<template>
<div class="al-view rounded">
<div>
<Header :album="album.info" />
</div>
<div class="separator" id="av-sep"></div>
<div class="songs rounded">
<SongList :tracks="album.tracks" :on_album_page="true"/>
</div>
<div class="separator" id="av-sep"></div>
<FeaturedArtists :artists="album.artists" />
<div v-if="album.bio">
<div class="separator" id="av-sep"></div>
<AlbumBio :bio="album.bio" />
<div class="al-content" id="albumcontent">
<div>
<Header :album="album.info" @resetBottomPadding="resetBottomPadding" />
</div>
<div class="songs rounded">
<SongList :tracks="album.tracks" :on_album_page="true" />
</div>
<div
id="bottom-items"
class="rounded"
ref="albumbottomcards"
: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>
</template>
@@ -22,25 +40,76 @@ import AlbumBio from "../components/AlbumView/AlbumBio.vue";
import SongList from "../components/FolderView/SongList.vue";
import FeaturedArtists from "../components/PlaylistView/FeaturedArtists.vue";
import useAStore from "../stores/pages/album";
import { onBeforeRouteUpdate } from "vue-router";
import { onMounted, ref } from "vue";
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) => {
await album.fetchTracksAndArtists(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>
<style lang="scss">
.al-view {
scrollbar-width: none;
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 {
min-height: calc(100% - 31.5rem);
margin-top: $small;
}
&::-webkit-scrollbar {
@@ -50,5 +119,65 @@ onBeforeRouteUpdate(async (to) => {
#av-sep {
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>
+2 -2
View File
@@ -7,7 +7,7 @@
- [ ] Add and deploy demo branch
### 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.
- [ ] Make dummy buttons functional.
- [ ] 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 { defineConfig } from "vite";
import svgLoader from "vite-svg-loader";
const path = require("path");