↩️ Merge pull request #59 from geoffrey45/fuzzy-search

Implement Fuzzy search using rapidfuzz
This commit is contained in:
Mungai Geoffrey
2022-05-26 19:15:09 +03:00
committed by GitHub
69 changed files with 1524 additions and 997 deletions
+3
View File
@@ -9,7 +9,10 @@
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src" "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^8.5.0",
"@vueuse/motion": "^2.0.0-beta.18",
"axios": "^0.26.1", "axios": "^0.26.1",
"defu": "^6.0.0",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"node-vibrant": "^3.2.1-alpha.1", "node-vibrant": "^3.2.1-alpha.1",
"pinia": "^2.0.11", "pinia": "^2.0.11",
+88 -31
View File
@@ -1,11 +1,10 @@
""" """
Contains all the search routes. Contains all the search routes.
""" """
from flask import Blueprint, request
from app.lib import searchlib
from app import helpers from app import helpers
from app.lib import searchlib
from flask import Blueprint
from flask import request
search_bp = Blueprint("search", __name__, url_prefix="/") search_bp = Blueprint("search", __name__, url_prefix="/")
@@ -16,6 +15,63 @@ SEARCH_RESULTS = {
} }
@search_bp.route("/search/tracks", methods=["GET"])
def search_tracks():
"""
Searches for tracks.
"""
query = request.args.get("q")
if not query:
return {"error": "No query provided"}, 400
results = searchlib.SearchTracks(query)()
SEARCH_RESULTS["tracks"] = results
return {
"tracks": results[:5],
"more": len(results) > 5,
}, 200
@search_bp.route("/search/albums", methods=["GET"])
def search_albums():
"""
Searches for albums.
"""
query = request.args.get("q")
if not query:
return {"error": "No query provided"}, 400
results = searchlib.SearchAlbums(query)()
SEARCH_RESULTS["albums"] = results
return {
"albums": results[:6],
"more": len(results) > 6,
}, 200
@search_bp.route("/search/artists", methods=["GET"])
def search_artists():
"""
Searches for artists.
"""
query = request.args.get("q")
if not query:
return {"error": "No query provided"}, 400
results = searchlib.SearchArtists(query)()
SEARCH_RESULTS["artists"] = results
return {
"artists": results[:6],
"more": len(results) > 6,
}, 200
@search_bp.route("/search") @search_bp.route("/search")
def search(): def search():
""" """
@@ -23,25 +79,14 @@ def search():
""" """
query = request.args.get("q") or "Mexican girl" query = request.args.get("q") or "Mexican girl"
albums = searchlib.get_search_albums(query) albums = searchlib.SearchAlbums(query)()
artists_dicts = [] artists_dicts = searchlib.SearchArtists(query)()
artist_tracks = searchlib.get_artists(query) tracks = searchlib.SearchTracks(query)()
top_artist = artists_dicts[0]["name"]
for song in artist_tracks: _tracks = searchlib.GetTopArtistTracks(top_artist)()
for artist in song.artists: tracks = [*tracks, *[t for t in _tracks if t not in tracks]]
if query.lower() in artist.lower():
artist_obj = {
"name": artist,
"image": helpers.check_artist_image(artist),
}
if artist_obj not in artists_dicts:
artists_dicts.append(artist_obj)
_tracks = searchlib.get_tracks(query)
tracks = [*_tracks, *artist_tracks]
SEARCH_RESULTS.clear() SEARCH_RESULTS.clear()
SEARCH_RESULTS["tracks"] = tracks SEARCH_RESULTS["tracks"] = tracks
@@ -50,9 +95,18 @@ def search():
return { return {
"data": [ "data": [
{"tracks": tracks[:5], "more": len(tracks) > 5}, {
{"albums": albums[:6], "more": len(albums) > 6}, "tracks": tracks[:5],
{"artists": artists_dicts[:6], "more": len(artists_dicts) > 6}, "more": len(tracks) > 5
},
{
"albums": albums[:6],
"more": len(albums) > 6
},
{
"artists": artists_dicts[:6],
"more": len(artists_dicts) > 6
},
] ]
} }
@@ -63,22 +117,25 @@ def search_load_more():
Returns more songs, albums or artists from a search query. Returns more songs, albums or artists from a search query.
""" """
type = request.args.get("type") type = request.args.get("type")
start = int(request.args.get("start")) index = int(request.args.get("index"))
print(type, index)
print(len(SEARCH_RESULTS["tracks"]))
if type == "tracks": if type == "tracks":
return { return {
"tracks": SEARCH_RESULTS["tracks"][start : start + 5], "tracks": SEARCH_RESULTS["tracks"][index:index + 5],
"more": len(SEARCH_RESULTS["tracks"]) > start + 5, "more": len(SEARCH_RESULTS["tracks"]) > index + 5,
} }
elif type == "albums": elif type == "albums":
return { return {
"albums": SEARCH_RESULTS["albums"][start : start + 6], "albums": SEARCH_RESULTS["albums"][index:index + 6],
"more": len(SEARCH_RESULTS["albums"]) > start + 6, "more": len(SEARCH_RESULTS["albums"]) > index + 6,
} }
elif type == "artists": elif type == "artists":
return { return {
"artists": SEARCH_RESULTS["artists"][start : start + 6], "artists": SEARCH_RESULTS["artists"][index:index + 6],
"more": len(SEARCH_RESULTS["artists"]) > start + 6, "more": len(SEARCH_RESULTS["artists"]) > index + 6,
} }
+16 -7
View File
@@ -1,11 +1,11 @@
""" """
This module contains mini functions for the server. This module contains mini functions for the server.
""" """
from datetime import datetime
import os import os
import random import random
import threading import threading
import time import time
from datetime import datetime
from typing import Dict from typing import Dict
from typing import List from typing import List
@@ -27,7 +27,9 @@ def background(func):
return background_func return background_func
def run_fast_scandir(__dir: str, ext: list, full=False) -> Dict[List[str], List[str]]: def run_fast_scandir(__dir: str,
ext: list,
full=False) -> Dict[List[str], List[str]]:
""" """
Scans a directory for files with a specific extension. Returns a list of files and folders in the directory. Scans a directory for files with a specific extension. Returns a list of files and folders in the directory.
""" """
@@ -60,12 +62,10 @@ def remove_duplicates(tracklist: List[models.Track]) -> List[models.Track]:
while song_num < len(tracklist) - 1: while song_num < len(tracklist) - 1:
for index, song in enumerate(tracklist): for index, song in enumerate(tracklist):
if ( if (tracklist[song_num].title == song.title
tracklist[song_num].title == song.title
and tracklist[song_num].album == song.album and tracklist[song_num].album == song.album
and tracklist[song_num].artists == song.artists and tracklist[song_num].artists == song.artists
and index != song_num and index != song_num):
):
tracklist.remove(song) tracklist.remove(song)
song_num += 1 song_num += 1
@@ -108,7 +108,8 @@ def check_artist_image(image: str) -> str:
""" """
img_name = image.replace("/", "::") + ".webp" img_name = image.replace("/", "::") + ".webp"
if not os.path.exists(os.path.join(app_dir, "images", "artists", img_name)): if not os.path.exists(os.path.join(app_dir, "images", "artists",
img_name)):
return use_memoji() return use_memoji()
else: else:
return img_name return img_name
@@ -120,7 +121,15 @@ def create_album_hash(title: str, artist: str) -> str:
""" """
return (title + artist).replace(" ", "").lower() return (title + artist).replace(" ", "").lower()
def create_new_date(): def create_new_date():
now = datetime.now() now = datetime.now()
str = now.strftime("%Y-%m-%d %H:%M:%S") str = now.strftime("%Y-%m-%d %H:%M:%S")
return str return str
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 '/\\:*?"<>|'])
+9 -2
View File
@@ -6,13 +6,13 @@ import urllib
from typing import List from typing import List
from app import api from app import api
from app import helpers
from app import instances from app import instances
from app import models from app import models
from app.lib import taglib from app.lib import taglib
from app.lib import trackslib from app.lib import trackslib
from progress.bar import Bar from progress.bar import Bar
from tqdm import tqdm from tqdm import tqdm
from app import helpers
def get_all_albums() -> List[models.Album]: def get_all_albums() -> List[models.Album]:
@@ -52,6 +52,10 @@ def create_everything() -> List[models.Track]:
def find_album(albums: List[models.Album], hash: str) -> int | None: def find_album(albums: List[models.Album], hash: str) -> int | None:
""" """
Finds an album by album title and artist. Finds an album by album title and artist.
:param `albums`: List of album objects.
:param `hash`: Hash of album.
:return: Index of album in list.
""" """
left = 0 left = 0
@@ -62,8 +66,11 @@ def find_album(albums: List[models.Album], hash: str) -> int | None:
iter += 1 iter += 1
mid = (left + right) // 2 mid = (left + right) // 2
try:
if albums[mid].hash == hash: if albums[mid].hash == hash:
return mid return mid
except AttributeError:
print(albums)
if albums[mid].hash < hash: if albums[mid].hash < hash:
left = mid + 1 left = mid + 1
@@ -151,7 +158,7 @@ def get_album_tracks(album: str, artist: str) -> List:
return GetAlbumTracks(album, artist).find_tracks() return GetAlbumTracks(album, artist).find_tracks()
def create_album(track: dict, tracklist: list) -> models.Album: def create_album(track: dict, tracklist: list) -> dict:
""" """
Generates and returns an album object from a track object. Generates and returns an album object from a track object.
""" """
+36 -18
View File
@@ -1,25 +1,26 @@
import os
import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy from copy import deepcopy
from multiprocessing import Pool
from os import path from os import path
import time
from typing import List from typing import List
from tqdm import tqdm
from app import api from app import api
from app import settings from app import settings
from app.helpers import create_album_hash, run_fast_scandir from app.helpers import create_album_hash
from app.helpers import run_fast_scandir
from app.instances import album_instance from app.instances import album_instance
from app.instances import tracks_instance from app.instances import tracks_instance
from app.lib import folderslib from app.lib import folderslib
from app.lib.albumslib import create_album from app.lib.albumslib import create_album
from app.lib.albumslib import find_album from app.lib.albumslib import find_album
from app.lib.taglib import get_tags from app.lib.taglib import get_tags
from app.logger import Log
from app.models import Album, Track
from app.lib.trackslib import find_track from app.lib.trackslib import find_track
from app.logger import Log
from app.models import Album
from app.models import Track
from tqdm import tqdm
class Populate: class Populate:
@@ -46,7 +47,7 @@ class Populate:
def run(self): def run(self):
self.check_untagged() self.check_untagged()
self.tag_all_files() self.get_all_tags()
if len(self.tagged_tracks) == 0: if len(self.tagged_tracks) == 0:
return return
@@ -76,6 +77,17 @@ class Populate:
Log(f"Found {len(self.files)} untagged tracks") Log(f"Found {len(self.files)} untagged tracks")
def process_tags(self, tags: dict):
for t in tags:
if t is None:
continue
t["albumhash"] = create_album_hash(t["album"], t["albumartist"])
self.tagged_tracks.append(t)
api.DB_TRACKS.append(t)
self.folders.add(t["folder"])
def get_tags(self, file: str): def get_tags(self, file: str):
tags = get_tags(file) tags = get_tags(file)
@@ -83,19 +95,27 @@ class Populate:
folder = tags["folder"] folder = tags["folder"]
self.folders.add(folder) self.folders.add(folder)
tags["albumhash"] = create_album_hash(tags["album"], tags["albumartist"]) tags["albumhash"] = create_album_hash(tags["album"],
tags["albumartist"])
self.tagged_tracks.append(tags) self.tagged_tracks.append(tags)
api.DB_TRACKS.append(tags) api.DB_TRACKS.append(tags)
def tag_all_files(self): def get_all_tags(self):
""" """
Loops through all the untagged files and tags them. Loops through all the untagged files and tags them.
""" """
s = time.time() s = time.time()
print(f"Started tagging files") # print(f"Started tagging files")
with ThreadPoolExecutor() as executor: # with ThreadPoolExecutor() as executor:
executor.map(self.get_tags, self.files) # executor.map(self.get_tags, self.files)
with Pool(maxtasksperchild=10) as p:
tags = p.map(get_tags, tqdm(self.files))
self.process_tags(tags)
# for t in tqdm(self.files):
# self.get_tags(t)
d = time.time() - s d = time.time() - s
Log(f"Tagged {len(self.tagged_tracks)} files in {d} seconds") Log(f"Tagged {len(self.tagged_tracks)} files in {d} seconds")
@@ -149,8 +169,7 @@ class Populate:
for album in tqdm(self.pre_albums, desc="Building albums"): for album in tqdm(self.pre_albums, desc="Building albums"):
self.create_album(album) self.create_album(album)
Log( Log(f"{self.exist_count} of {len(self.pre_albums)} albums were already in the database"
f"{self.exist_count} of {len(self.pre_albums)} albums were already in the database"
) )
def create_track(self, track: dict): def create_track(self, track: dict):
@@ -186,8 +205,7 @@ class Populate:
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
executor.map(self.create_track, self.tagged_tracks) executor.map(self.create_track, self.tagged_tracks)
Log( Log(f"Added {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums"
f"Added {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums"
) )
def save_albums(self): def save_albums(self):
+147 -7
View File
@@ -1,19 +1,156 @@
""" """
This library contains all the functions related to the search functionality. This library contains all the functions related to the search functionality.
""" """
from typing import List from typing import List
from app import models, helpers
from app.lib import albumslib
from app import api from app import api
from app import helpers
from app import models
from app.lib import albumslib
from rapidfuzz import fuzz
from rapidfuzz import process
ratio = fuzz.ratio
wratio = fuzz.WRatio
def get_tracks(query: str) -> List[models.Track]: class Cutoff:
"""
Holds all the default cutoff values.
"""
tracks: int = 70
albums: int = 70
artists: int = 70
class Limit:
"""
Holds all the default limit values.
"""
tracks: int = 50
albums: int = 50
artists: int = 50
class SearchTracks:
def __init__(self, query) -> None:
self.query = query
def __call__(self) -> List[models.Track]:
""" """
Gets all songs with a given title. Gets all songs with a given title.
""" """
tracks = [track for track in api.TRACKS if query.lower() in track.title.lower()]
return helpers.remove_duplicates(tracks) tracks = [track.title for track in api.TRACKS]
results = process.extract(
self.query,
tracks,
scorer=fuzz.WRatio,
score_cutoff=Cutoff.tracks,
limit=Limit.tracks,
)
return [api.TRACKS[i[2]] for i in results]
class SearchArtists:
def __init__(self, query) -> None:
self.query = query
@staticmethod
def get_all_artist_names() -> List[str]:
"""
Gets all artist names.
"""
artists = [track.artists for track in api.TRACKS]
f_artists = set()
for artist in artists:
for a in artist:
f_artists.add(a)
return f_artists
def __call__(self) -> list:
"""
Gets all artists with a given name.
"""
artists = self.get_all_artist_names()
results = process.extract(
self.query,
artists,
scorer=fuzz.WRatio,
score_cutoff=Cutoff.artists,
limit=Limit.artists,
)
f_artists = []
for artist in results:
aa = {
"name": artist[0],
"image": helpers.create_safe_name(artist[0]) + ".webp",
}
f_artists.append(aa)
return f_artists
class SearchAlbums:
def __init__(self, query) -> None:
self.query = query
def get_albums_by_name(self) -> List[models.Album]:
"""
Gets all albums with a given title.
"""
albums = [album.title for album in api.ALBUMS]
results = process.extract(self.query, albums)
return [api.ALBUMS[i[2]] for i in results]
def __call__(self) -> List[models.Album]:
"""
Gets all albums with a given title.
"""
albums = [a.title for a in api.ALBUMS]
results = process.extract(
self.query,
albums,
scorer=fuzz.WRatio,
score_cutoff=Cutoff.albums,
limit=Limit.albums,
)
return [api.ALBUMS[i[2]] for i in results]
# get all artists that matched the query
# for get all albums from the artists
# get all albums that matched the query
# return [**artist_albums **albums]
# recheck next and previous artist on play next or add to playlist
class GetTopArtistTracks:
def __init__(self, artist: str) -> None:
self.artist = artist
def __call__(self) -> List[models.Track]:
"""
Gets all tracks from a given artist.
"""
return [track for track in api.TRACKS if self.artist in track.artists]
def get_search_albums(query: str) -> List[models.Album]: def get_search_albums(query: str) -> List[models.Album]:
@@ -27,4 +164,7 @@ def get_artists(artist: str) -> List[models.Track]:
""" """
Gets all songs with a given artist. Gets all songs with a given artist.
""" """
return [track for track in api.TRACKS if artist.lower() in str(track.artists).lower()] return [
track for track in api.TRACKS
if artist.lower() in str(track.artists).lower()
]
+1 -1
View File
@@ -154,7 +154,7 @@ def parse_disk_number(audio):
return disk_number return disk_number
def get_tags(fullpath: str) -> dict: def get_tags(fullpath: str) -> dict | None:
""" """
Returns a dictionary of tags for a given file. Returns a dictionary of tags for a given file.
""" """
+17 -8
View File
@@ -7,14 +7,14 @@ import time
from app import api from app import api
from app import instances from app import instances
from app import models from app import models
from app.helpers import create_album_hash
from app.lib import folderslib from app.lib import folderslib
from app.lib.albumslib import create_album, find_album from app.lib.albumslib import create_album
from app.lib.albumslib import find_album
from app.lib.taglib import get_tags from app.lib.taglib import get_tags
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer from watchdog.observers import Observer
from app.helpers import create_album_hash
class OnMyWatch: class OnMyWatch:
""" """
@@ -50,18 +50,19 @@ def add_track(filepath: str) -> None:
tags = get_tags(filepath) tags = get_tags(filepath)
if tags is not None: if tags is not None:
tags["albumhash"] = create_album_hash(tags["album"], tags["albumartist"]) hash = create_album_hash(tags["album"], tags["albumartist"])
tags["albumhash"] = hash
api.DB_TRACKS.append(tags) api.DB_TRACKS.append(tags)
albumindex = find_album(tags["album"], tags["albumartist"]) albumindex = find_album(api.ALBUMS, hash)
if albumindex is not None: if albumindex is not None:
album = api.ALBUMS[albumindex] album = api.ALBUMS[albumindex]
else: else:
album_data = create_album(tags, api.DB_TRACKS) album_data = create_album(tags, api.DB_TRACKS)
instances.album_instance.insert_album(album_data)
album = models.Album(album_data) album = models.Album(album_data)
instances.album_instance.insert_album(album)
api.ALBUMS.append(album) api.ALBUMS.append(album)
tags["image"] = album.image tags["image"] = album.image
@@ -86,7 +87,8 @@ def remove_track(filepath: str) -> None:
fpath = filepath.replace(fname, "") fpath = filepath.replace(fname, "")
try: try:
trackid = instances.tracks_instance.get_song_by_path(filepath)["_id"]["$oid"] trackid = instances.tracks_instance.get_song_by_path(
filepath)["_id"]["$oid"]
except TypeError: except TypeError:
print(f"💙 Watchdog Error: Error removing track {filepath} TypeError") print(f"💙 Watchdog Error: Error removing track {filepath} TypeError")
return return
@@ -152,7 +154,14 @@ class Handler(PatternMatchingEventHandler):
Fired when a created file is closed. Fired when a created file is closed.
""" """
print("⚫ closed ~~~") print("⚫ closed ~~~")
try:
self.files_to_process.remove(event.src_path) self.files_to_process.remove(event.src_path)
except ValueError:
"""
The file was already removed from the list, or it was not in the list to begin with.
"""
pass
add_track(event.src_path) add_track(event.src_path)
+24 -7
View File
@@ -6,8 +6,8 @@ from dataclasses import field
from typing import List from typing import List
from app import api from app import api
from app.exceptions import TrackExistsInPlaylist
from app import helpers from app import helpers
from app.exceptions import TrackExistsInPlaylist
@dataclass(slots=True) @dataclass(slots=True)
@@ -18,7 +18,7 @@ class Track:
trackid: str trackid: str
title: str title: str
artists: str artists: list
albumartist: str albumartist: str
album: str album: str
folder: str folder: str
@@ -32,8 +32,11 @@ class Track:
albumhash: str albumhash: str
def __init__(self, tags): def __init__(self, tags):
try:
self.trackid = tags["_id"]["$oid"] self.trackid = tags["_id"]["$oid"]
except KeyError:
print(tags)
self.title = tags["title"] self.title = tags["title"]
self.artists = tags["artists"].split(", ") self.artists = tags["artists"].split(", ")
self.albumartist = tags["albumartist"] self.albumartist = tags["albumartist"]
@@ -49,6 +52,22 @@ class Track:
self.albumhash = tags["albumhash"] self.albumhash = tags["albumhash"]
@dataclass(slots=True)
class Artist:
"""
Artist class
"""
artistid: str
name: str
image: str
def __init__(self, tags):
self.artistid = tags["_id"]["$oid"]
self.name = tags["name"]
self.image = tags["image"]
@dataclass @dataclass
class Album: class Album:
""" """
@@ -78,11 +97,9 @@ class Album:
def get_p_track(ptrack): def get_p_track(ptrack):
for track in api.TRACKS: for track in api.TRACKS:
if ( if (track.title == ptrack["title"]
track.title == ptrack["title"]
and track.artists == ptrack["artists"] and track.artists == ptrack["artists"]
and ptrack["album"] == track.album and ptrack["album"] == track.album):
):
return track return track
+153 -1
View File
@@ -118,6 +118,14 @@ category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "jarowinkler"
version = "1.0.2"
description = "library for fast approximate string matching using Jaro and Jaro-Winkler similarity"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.0.3" version = "3.0.3"
@@ -181,6 +189,20 @@ snappy = ["python-snappy"]
srv = ["dnspython (>=1.16.0,<3.0.0)"] srv = ["dnspython (>=1.16.0,<3.0.0)"]
zstd = ["zstandard"] zstd = ["zstandard"]
[[package]]
name = "rapidfuzz"
version = "2.0.11"
description = "rapid fuzzy string matching"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
jarowinkler = ">=1.0.2,<1.1.0"
[package.extras]
full = ["numpy"]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.27.1" version = "2.27.1"
@@ -262,7 +284,7 @@ watchdog = ["watchdog"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "a2a0355e78fe2881e226dabda21b6d972a98aa4f1d60bf9f7f74957bb8ba6bea" content-hash = "143a7a7f2158b3c3abfcfec207443a90f1a3d31a6935dfdf5e5e7452e226ca58"
[metadata.files] [metadata.files]
certifi = [ certifi = [
@@ -309,6 +331,88 @@ itsdangerous = [
{file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"},
{file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"},
] ]
jarowinkler = [
{file = "jarowinkler-1.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:71772fcd787e0286b779de0f1bef1e0a25deb4578328c0fc633bc345f13ffd20"},
{file = "jarowinkler-1.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:912ee0a465822a8d659413cebc1ab9937ac5850c9cd1e80be478ba209e7c8095"},
{file = "jarowinkler-1.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0320f7187dced1ad413bf2c3631ec47567e65dfdea92c523aafb2c085ae15035"},
{file = "jarowinkler-1.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58bc6a8f01b0dfdf3721f9a4954060addeccf8bbe5e72a71cf23a88ce0d30440"},
{file = "jarowinkler-1.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:679ec7a42f70baa61f3a214d1b59cec90fc036021c759722075efcc8697e7b1f"},
{file = "jarowinkler-1.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dde57d47962d6a4436d8a3b477bcc8233c6da28e675027eb3a490b0d6dc325be"},
{file = "jarowinkler-1.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:657f50204970fac8f120c293e52a3451b742c9b26125010405ec7365cb6e2a49"},
{file = "jarowinkler-1.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04f18a7398766b36ffbe4bcd26d34fcd6ed01f4f2f7eea13e316e6cca0e10c98"},
{file = "jarowinkler-1.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33a24b380e2c076eabf2d3e12eee56b6bf10b1f326444e18c36a495387dbf0de"},
{file = "jarowinkler-1.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e1d7d6e6c98fb785026584373240cc4076ad21033f508973faae05e846206e8c"},
{file = "jarowinkler-1.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e50c750a45c800d91134200d8cbf746258ed357a663e97cc0348ee42a948386a"},
{file = "jarowinkler-1.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:5b380afce6cdc25a4dafd86874f07a393800577c05335c6ad67ccda41db95c60"},
{file = "jarowinkler-1.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e73712747ac5d2218af3ed3c1600377f18a0a45af95f22c39576165aea2908b4"},
{file = "jarowinkler-1.0.2-cp310-cp310-win32.whl", hash = "sha256:9511f4e1f00c822e08dbffeb69e15c75eb294a5f24729815a97807ecf03d22eb"},
{file = "jarowinkler-1.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a5c44f92e9ac6088286292ecb69e970adc2b98e139b8923bce9bbb9d484e6a0f"},
{file = "jarowinkler-1.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:02b0bf34ffc2995b695d9b10d2f18c1c447fbbdb7c913a84a0a48c186ccca3b8"},
{file = "jarowinkler-1.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7a8e45176298a1210c06f8b2328030cc3c93a45dab068ac1fbc9cf075cd95b"},
{file = "jarowinkler-1.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da27a9c206249a50701bfa5cfbbb3a04236e1145b2b0967e825438acb14269bf"},
{file = "jarowinkler-1.0.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43ea0155379df92021af0f4a32253be3953dfa0f050ec3515f314b8f48a96674"},
{file = "jarowinkler-1.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f33b6b1687db1be1abba60850628ee71547501592fcf3504e021274bc5ccb7a"},
{file = "jarowinkler-1.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff304de32ee6acd5387103a0ad584060d8d419aa19cbbeca95204de9c4f01171"},
{file = "jarowinkler-1.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:662dd6f59cca536640be0cda32c901989504d95316b192e6aa41d098fa08c795"},
{file = "jarowinkler-1.0.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:01f85abb75fa43e98db34853d35570d98495ee2fcbbf45a93838e0289c162f19"},
{file = "jarowinkler-1.0.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5b9332dcc8130af4101c9752a03e977c54b8c12982a2a3ca4c2e4cc542accc00"},
{file = "jarowinkler-1.0.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:af765b037404a536c372e33ddd4c430aea28f1d82a8ef51a2955442b8b690577"},
{file = "jarowinkler-1.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aea2c7d66b57c56d00f9c45ae7862d86e3ae84368ecea17f3552c0052a7f3bcf"},
{file = "jarowinkler-1.0.2-cp36-cp36m-win32.whl", hash = "sha256:8b1288a09a8d100e9bf7cf9ce1329433db73a0d0350d74c2c6f5c31ac69096cf"},
{file = "jarowinkler-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ed39199b0e806902347473c65e5c05933549cf7e55ba628c6812782f2c310b19"},
{file = "jarowinkler-1.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:473b057d7e5a0f5e5b8c0e0f7960d3ca2f2954c3c93fd7a9fb2cc4bc3cc940fb"},
{file = "jarowinkler-1.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdb892dbbbd77b3789a10b2ce5e8acfe5821cc6423e835bae2b489159f3c2211"},
{file = "jarowinkler-1.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:012a8333328ce061cba1ff081843c8d80eb1afe8fa2889ad29d767ea3fdc7562"},
{file = "jarowinkler-1.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3421120c07ee6d3f59c5adde32eb9a050cfd1b3666b0e2d8c337d934a9d091f9"},
{file = "jarowinkler-1.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dad57327cc90f8daa3afb98e2d274d7dd1b60651f32717449be95d3b3366d61a"},
{file = "jarowinkler-1.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4fd1757eff43df97227fd63d9c8078582267a0b25cefef6f6a64d3e46e80ba2"},
{file = "jarowinkler-1.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:32269ebbcb860f01c055d9bb145b4cc91990f62c7644a85b21458b4868621113"},
{file = "jarowinkler-1.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3b5a0839e84f5ff914b01b5b94d0273954affce9cc2b2ee2c31fe2fcb9c8ae76"},
{file = "jarowinkler-1.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:6c9d3a9ef008428b5dce2855eebe2b6127ea7a7e433aedf240653fad4bd4baa6"},
{file = "jarowinkler-1.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a3d7759d8a66ee05595bde012f93da8a63499f38205e2bb47022c52bd6c47108"},
{file = "jarowinkler-1.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2ba1b1b0bf45042a9bbb95d272fd8b0c559fe8f6806f088ec0372899e1bc6224"},
{file = "jarowinkler-1.0.2-cp37-cp37m-win32.whl", hash = "sha256:4cb33f4343774d69abf8cf65ad57919e7a171c44ba6ad57b08147c3f0f06b073"},
{file = "jarowinkler-1.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:0392b72ddb5ab5d6c1d5df94dbdac7bf229670e5e64b2b9a382d02d6158755e5"},
{file = "jarowinkler-1.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:94f663ad85bc7a89d7e8b6048f93a46d2848a0570ab07fc895a239b9a5d97b93"},
{file = "jarowinkler-1.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:895a10766ff3db15e7cf2b735e4277bee051eaafb437aaaef2c5de64a5c3f05c"},
{file = "jarowinkler-1.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0c1a84e770b3ec7385a4f40efb30bdc96f96844564f91f8d3937d54a8969d82c"},
{file = "jarowinkler-1.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27defe81d76e02b3929322baea999f5232837e7f308c2dc5b37de7568c2bc583"},
{file = "jarowinkler-1.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:158f117481388f8d23fe4bd2567f37be0ccae0f4631c34e4b0345803147da207"},
{file = "jarowinkler-1.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:427c675b4f3e83c79a4b6af7441f29e30a173c7a0ae72a54f51090eee7a8ae02"},
{file = "jarowinkler-1.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90a7f3fd173339bc62e52c02f43d50c947cb3af9cda41646e218aea13547e0c2"},
{file = "jarowinkler-1.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3975cbe8b6ae13fc63d74bcbed8dac1577078d8cd8728e60621fe75885d2a8c5"},
{file = "jarowinkler-1.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:141840f33345b00abd611839080edc99d4d31abd2dcf701a3e50c90f9bfb2383"},
{file = "jarowinkler-1.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f592f9f6179e347a5f518ca7feb9bf3ac068f2fad60ece5a0eef5e5e580d4c8b"},
{file = "jarowinkler-1.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:30565d70396eb9d1eb622e1e707ddc2f3b7a9692558b8bf4ea49415a5ca2f854"},
{file = "jarowinkler-1.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:35fc430c11b80a43ed826879c78c4197ec665d5150745b3668bec961acf8a757"},
{file = "jarowinkler-1.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf4b7090f0c4075bec1638717f54b22c3b0fe733dc87146a19574346ed3161"},
{file = "jarowinkler-1.0.2-cp38-cp38-win32.whl", hash = "sha256:199f4f7edbc49439a97440caa1e244d2e33da3e16d7b0afce4e4dfd307e555c7"},
{file = "jarowinkler-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:b587e8fdd96cc470d6bdf428129c65264731b09b5db442e2d092e983feec4aab"},
{file = "jarowinkler-1.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4b233180b3e2f2d7967aa570d36984e9d2ec5a9067c0d1c44cd3b805d9da9363"},
{file = "jarowinkler-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2220665a1f52262ae8b76e3baf474ebcd209bfcb6a7cada346ffd62818f5aa3e"},
{file = "jarowinkler-1.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08c98387e04e749c84cc967db628e5047843f19f87bf515a35b72f7050bc28ad"},
{file = "jarowinkler-1.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d710921657442ad3c942de684aba0bdf16b7de5feed3223b12f3b2517cf17f7c"},
{file = "jarowinkler-1.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:401c02ac7245103826f54c816324274f53d50b638ab0f8b359a13055a7a6e793"},
{file = "jarowinkler-1.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a1929a0029f208cc9244499dc93b4d52ee8e80d2849177d425cf6e0be1ea781"},
{file = "jarowinkler-1.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab25d147be9b04e7de2d28a18e72fadc152698c3e51683c6c61f73ffbae2f9e"},
{file = "jarowinkler-1.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:465cfdff355ec9c55f65fd1e1315260ec20c8cff0eb90d9f1a0ad8d503dc002b"},
{file = "jarowinkler-1.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:29ef1113697cc74c2f04bc15008abbd726cb2d5b01c040ba87c6cb7abd1d0e0d"},
{file = "jarowinkler-1.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:61b57c8b36361ec889f99f761441bb0fa21b850a5eb3305dea25fef68f6a797b"},
{file = "jarowinkler-1.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ee9d9af1bbf194d78f4b69c2139807c23451068b27a053a1400d683d6f36c61d"},
{file = "jarowinkler-1.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a9b33b0ceb472bbc65683467189bd032c162256b2a137586ee3448a9f8f886ec"},
{file = "jarowinkler-1.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:582f6e213a6744883ced44482a51efcc21ae632defac27f12f6430a8e99b1070"},
{file = "jarowinkler-1.0.2-cp39-cp39-win32.whl", hash = "sha256:4d1c8f403016d5c0262de7a8588eee370c37a609e1f529f8407e99a70d020af7"},
{file = "jarowinkler-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:ab50ffa66aa201616871c1b90ac0790f56666118db3c8a8fcb3a7a6e03971510"},
{file = "jarowinkler-1.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8e59a289dcf93504ab92795666c39b2dbe98ac18655201992a7e6247de676bf4"},
{file = "jarowinkler-1.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c36eccdc866f06a7b35da701bd8f91e0dfc83b35c07aba75ce8c906cbafaf184"},
{file = "jarowinkler-1.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123163f01a5c43f12e4294e7ce567607d859e1446b1a43bd6cd404b3403ffa07"},
{file = "jarowinkler-1.0.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d41fdecd907189e47c7d478e558ad417da38bf3eb34cc20527035cb3fca3e2b8"},
{file = "jarowinkler-1.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7829368fc91de225f37f6325f8d8ec7ad831dc5b0e9547f1977e2fdc85eccc1"},
{file = "jarowinkler-1.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278595417974553a8fdf3c8cce5c2b4f859335344075b870ecb55cc416eb76cf"},
{file = "jarowinkler-1.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:208fc49741db5d3e6bbd4a2f7b32d32644b462bf205e7510eca4e2d530225f03"},
{file = "jarowinkler-1.0.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:924afcab6739c453f1c3492701d185d71dc0e5ba15692bd0bfa6d482c7e8f79e"},
{file = "jarowinkler-1.0.2.tar.gz", hash = "sha256:788ac33e6ffdbd78fd913b481e37cfa149288575f087a1aae1a4ce219cb1c654"},
]
jinja2 = [ jinja2 = [
{file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
{file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
@@ -513,6 +617,54 @@ pymongo = [
{file = "pymongo-4.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:65f8a93816dcb2202710839907759aca9eece94d9f13215686f224fcc8966f9e"}, {file = "pymongo-4.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:65f8a93816dcb2202710839907759aca9eece94d9f13215686f224fcc8966f9e"},
{file = "pymongo-4.0.1.tar.gz", hash = "sha256:13d0624c13a91da71fa0d960205d93b3d98344481be865ee7cc238c972d41d73"}, {file = "pymongo-4.0.1.tar.gz", hash = "sha256:13d0624c13a91da71fa0d960205d93b3d98344481be865ee7cc238c972d41d73"},
] ]
rapidfuzz = [
{file = "rapidfuzz-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb54edd0fa8620d37a7c0762895260bc75a6cc083d161b14d40a562b6f303975"},
{file = "rapidfuzz-2.0.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8093b5f234be618bb8cfe34d65c072fee362fbd13f6c1b37f80eac0f30c24cfa"},
{file = "rapidfuzz-2.0.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea72586c08ba8ce08c37c21bb7c383df740dc7d6e921423e1881570be62ed15"},
{file = "rapidfuzz-2.0.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ee9a057b7638e91377b217df3724d4adefec3936617180b3df1f64fa64cd995"},
{file = "rapidfuzz-2.0.11-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bad453a76f6832a99251beb89c352a4f436f4e7687a5843b080c294dba68d8d6"},
{file = "rapidfuzz-2.0.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a545627a7d45ea4ad1cb66fb6ad7b951825b0e97053056cda9d2f5fbb30abe3e"},
{file = "rapidfuzz-2.0.11-cp310-cp310-win32.whl", hash = "sha256:8e583595efe5afdd68a7b5423cbd5fff0d1870d60cee16af17897f701f39d933"},
{file = "rapidfuzz-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:09efd5a02a33dfb18ec6f28b85f102b51cbac080e624924f3a4f36d3b08962ef"},
{file = "rapidfuzz-2.0.11-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c81216ecff325765bb441caf7f50a1f55aa66192aca12ec6d448b509c9387a39"},
{file = "rapidfuzz-2.0.11-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b555dbebb413ab66c2cd394338c860094a5464f9b63faeb40ebec44271c460b"},
{file = "rapidfuzz-2.0.11-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a007fe85dfff7a961daba13884629dcd9ab45197a2fc40749a7e8f750e7715a4"},
{file = "rapidfuzz-2.0.11-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1031420c083681b41346267e26f9f76ef2c1544a0129fa67b07239d7a9ab9fd6"},
{file = "rapidfuzz-2.0.11-cp36-cp36m-win32.whl", hash = "sha256:4b16147122ec4c5e4a31131b8530e674ba1b3e74e2b43b73aedc6bd0021fcae6"},
{file = "rapidfuzz-2.0.11-cp36-cp36m-win_amd64.whl", hash = "sha256:de7559765e1da54d8d42495368e0a9852041cf8d4e077fb27811d6f009611a4b"},
{file = "rapidfuzz-2.0.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ace59b7857e5d5b252564dd60d840667c19c00d357c7ba32e9671b68615dc49a"},
{file = "rapidfuzz-2.0.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09d83e5ab57fb61a6003a3607494b1f443978e8d6b199fed3094e92f466f3bba"},
{file = "rapidfuzz-2.0.11-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3a53797613b53e93adbf9c410260aecde5ab1d7cd1b07792be1ee4800716598"},
{file = "rapidfuzz-2.0.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9084a550719aff3752e5a63e32d381d64b09264cabf35d5e21e6a9f0e91baca"},
{file = "rapidfuzz-2.0.11-cp37-cp37m-win32.whl", hash = "sha256:35c8f2cae3e2079616fdf90c6b6bcf850d3810c9184c6e89a4826b6d0af88974"},
{file = "rapidfuzz-2.0.11-cp37-cp37m-win_amd64.whl", hash = "sha256:1da580130b37a007684ab9dc6f85125e3c0d06c9f9df349e7bd52e312811c436"},
{file = "rapidfuzz-2.0.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:757ae64598a93d0f8a21007c1abd6800f38c04e4b89167ca7b833ce30f54aef3"},
{file = "rapidfuzz-2.0.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f5b0fd8f6bde8d89c07b76643c9f3a01e2e089b246a97b721e7fe97fdaa41820"},
{file = "rapidfuzz-2.0.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a13cea3303b444af49417352cd11830ea2245d4e5a82bb06b6895638b81c6029"},
{file = "rapidfuzz-2.0.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96dba6ef863cba7efd22077ba28e19a8829b523c7c7e41304c568a6ab91fc4d"},
{file = "rapidfuzz-2.0.11-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcecc662df808ad051d9524608a3682fd80d882c93664adbaab4c7b0796e385"},
{file = "rapidfuzz-2.0.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff74f3abd0ed473f81ba67d3207ca6db74b8b50ebae1a6734ba199a8d90d67b4"},
{file = "rapidfuzz-2.0.11-cp38-cp38-win32.whl", hash = "sha256:b3e7eea1dd304bac4ac74a1af71da35bb68bf6060f5d6b4ad8a3e4e2c84d5110"},
{file = "rapidfuzz-2.0.11-cp38-cp38-win_amd64.whl", hash = "sha256:5b203e83adc10dbe961a3000fa09cc47f5672d2c98c3fd2f6bb7b0df225805ee"},
{file = "rapidfuzz-2.0.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1fe837f8b305c59549e2eec6fe8dbdd7344eeef0033fa4ee90af65f72b32c25f"},
{file = "rapidfuzz-2.0.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc2c8aa23de4a0bef2162440f5095f606052c289059fbeb03180740783e25e6b"},
{file = "rapidfuzz-2.0.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ebb03a6a5171233958b6adcada00c7521186ea4b78b6652b99d94d5dbf59c809"},
{file = "rapidfuzz-2.0.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f21d8754ee49ee8da73ad5e746495a27fd29ab769f5e45ede4d8232955e0237"},
{file = "rapidfuzz-2.0.11-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23c79ca555f188445f2e054b40a89c82e5d21b22d34f00da6e7491a6d70feff5"},
{file = "rapidfuzz-2.0.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b10de1ea834be1f26d1f35f3e1d1f8c003c7951a7475ab9b28b5a62e9f6f0c0"},
{file = "rapidfuzz-2.0.11-cp39-cp39-win32.whl", hash = "sha256:1bda150aca38c4d4739780c3a99c190c05101839adc10ab7804ed86000440267"},
{file = "rapidfuzz-2.0.11-cp39-cp39-win_amd64.whl", hash = "sha256:f4ea654ef221a57b47523fe70d7423254dd285f73948b9d8c1215610d2a38e9e"},
{file = "rapidfuzz-2.0.11-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2542f0a3c4079b15c0485ece589e5a248633de84326e4a3ca63ea024a0b59775"},
{file = "rapidfuzz-2.0.11-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dca3c02511d23a58ef14f2fbcd7b311eae1bc40e3d36be493ef22b9572ebed1"},
{file = "rapidfuzz-2.0.11-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbb649a978fab0232cefdeb67321d853c676b3ebb7481b8b80030905a42d799"},
{file = "rapidfuzz-2.0.11-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:081415323e94e0016109715438d4ccb233ab038b09ba3cf79038e50601a410e9"},
{file = "rapidfuzz-2.0.11-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a80efb64f1b38c64f04f4ca6881c9684d7912dc9124ecbf953c9b541f935b33c"},
{file = "rapidfuzz-2.0.11-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ac8ab8106bc7b0ffab539baa5279a850c61b71ccad86dc11503bc084f6ac1af"},
{file = "rapidfuzz-2.0.11-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3933a1cfdf6ab4e059d8eb68460fa430a4f6be06431d1a8b05f7fecdd63e586e"},
{file = "rapidfuzz-2.0.11-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:537b72d954ff395cefc210fc7e41810a26e84ed7f1e93d0dffe3669277d6ea23"},
{file = "rapidfuzz-2.0.11-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2d167b1bf92a60eefbaea3abf646baa2a3aa7125595e129c8890072706a80ac"},
{file = "rapidfuzz-2.0.11.tar.gz", hash = "sha256:934b65fea75e3bd310d74903ec69ff3df061b3058ab5b7f49ab772958109bca8"},
]
requests = [ requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
+1
View File
@@ -18,6 +18,7 @@ Pillow = "^9.0.1"
Flask-Caching = "^1.10.1" Flask-Caching = "^1.10.1"
"colorgram.py" = "^1.2.0" "colorgram.py" = "^1.2.0"
tqdm = "^4.64.0" tqdm = "^4.64.0"
rapidfuzz = "^2.0.11"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
+15 -5
View File
@@ -5,8 +5,18 @@
#python manage.py #python manage.py
gpath=$(poetry run which gunicorn) gpath=$(poetry run which gunicorn)
cd app while getopts ':s' opt; do
"$gpath" -b 0.0.0.0:9877 -w 4 --threads=2 "imgserver:app" & case $opt in
echo "Booted image server" s)
cd ../ echo "🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴"
"$gpath" -b 0.0.0.0:9876 -w 1 --threads=4 "manage:create_app()" #--log-level=debug cd "./app"
"$gpath" -b 0.0.0.0:9877 -w 4 --threads=2 "imgserver:app" &
cd ../
;;
\?)
echo "Invalid option: -$OPTARG" >&2
;;
esac
done
"$gpath" -b 0.0.0.0:9876 -w 1 --threads=4 "manage:create_app()"
+11 -30
View File
@@ -2,13 +2,9 @@
<ContextMenu /> <ContextMenu />
<Modal /> <Modal />
<Notification /> <Notification />
<div class="l-container" :class="{ collapsed: collapsed }"> <div class="l-container">
<div class="l-sidebar"> <div class="l-sidebar rounded">
<div id="logo-container"> <Logo />
<router-link :to="{ name: 'Home' }" v-if="!collapsed"
><div class="logo"></div
></router-link>
</div>
<Navigation /> <Navigation />
<div class="l-album-art"> <div class="l-album-art">
<nowPlaying /> <nowPlaying />
@@ -24,7 +20,7 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import Navigation from "./components/LeftSidebar/Navigation.vue"; import Navigation from "./components/LeftSidebar/Navigation.vue";
import Main from "./components/RightSideBar/Main.vue"; import Main from "./components/RightSideBar/Main.vue";
@@ -38,13 +34,14 @@ import Modal from "./components/modal.vue";
import Notification from "./components/Notification.vue"; import Notification from "./components/Notification.vue";
import useQStore from "./stores/queue"; import useQStore from "./stores/queue";
import listenForKeyboardEvents from "./composables/keyboard"; import listenForKeyboardEvents from "./composables/keyboard";
import Logo from "./components/Logo.vue";
const RightSideBar = Main; const RightSideBar = Main;
const context_store = useContextStore(); const context_store = useContextStore();
const queue = useQStore(); const queue = useQStore();
const app_dom = document.getElementById("app"); const app_dom = document.getElementById("app");
queue.readQueueFromLocalStorage(); queue.readQueue();
listenForKeyboardEvents(queue); listenForKeyboardEvents(queue);
app_dom.addEventListener("click", (e) => { app_dom.addEventListener("click", (e) => {
@@ -55,38 +52,22 @@ app_dom.addEventListener("click", (e) => {
</script> </script>
<style lang="scss"> <style lang="scss">
@import "./assets/css/mixins.scss";
.l-sidebar { .l-sidebar {
position: relative; position: relative;
.l-album-art { .l-album-art {
width: calc(100% - 2rem);
position: absolute; position: absolute;
bottom: 0; bottom: 0;
margin-bottom: 1rem;
} }
} }
#logo-container {
position: relative;
height: 3.6rem;
display: flex;
align-items: center;
margin-bottom: 0.5rem;
#toggle {
position: absolute;
width: 3rem;
height: 100%;
background: url(./assets/icons/menu.svg) no-repeat center;
background-size: 2rem;
cursor: pointer;
}
}
.logo {
height: 4.5rem;
width: 15rem;
background: url(./assets/icons/logo.svg) no-repeat 1rem;
background-size: 9rem;
}
.r-sidebar { .r-sidebar {
&::-webkit-scrollbar { &::-webkit-scrollbar {
+2 -36
View File
@@ -8,45 +8,15 @@
height: 100%; height: 100%;
padding-right: $small; padding-right: $small;
@include phone-only { @include phone-only {
grid-template-columns: 1fr 9.2rem; grid-template-columns: 1fr 9.2rem;
} }
.info {
display: flex;
padding-top: $small;
margin-left: $small;
.art {
width: 3rem;
height: 3rem;
background-image: url("../../images/null.webp");
}
.separator {
margin: 2px;
}
.desc {
width: calc(100% - 5rem);
margin-left: $small;
display: flex;
align-items: center;
margin-top: -$small;
.artists {
font-size: 0.8rem;
color: $white;
}
}
}
.controlsx { .controlsx {
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
display: flex; display: grid;
grid-template-columns: 12rem 1fr 12rem;
align-items: center; align-items: center;
padding: $small; padding: $small;
@@ -55,10 +25,6 @@
align-items: center; align-items: center;
} }
.controls-bottom {
width: min-content;
}
.progress-bottom { .progress-bottom {
width: 100%; width: 100%;
+7 -10
View File
@@ -1,4 +1,5 @@
@import "../css/ProgressBar.scss"; @import "../css/ProgressBar.scss";
@import "mixins.scss";
:root { :root {
--separator: #ffffff46; --separator: #ffffff46;
@@ -20,6 +21,8 @@ body {
image-rendering: -webkit-optimize-contrast; image-rendering: -webkit-optimize-contrast;
} }
.heading { .heading {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
@@ -39,7 +42,6 @@ a {
} }
.border { .border {
// border: solid 1px $gray5;
background-color: $black; background-color: $black;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1); box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
} }
@@ -95,11 +97,11 @@ a {
} }
.l-sidebar { .l-sidebar {
width: 15rem; width: 17rem;
grid-area: l-sidebar; grid-area: l-sidebar;
padding-top: 0.5rem; background-color: $gray;
// border-right: solid 1px $gray3; margin: $small;
background-color: $black; padding: 1rem;
} }
.bottom-bar { .bottom-bar {
@@ -107,11 +109,6 @@ a {
height: 4rem; height: 4rem;
} }
.collapsed .l-sidebar {
width: 3rem;
transition: all 0.3s ease;
}
.ellip { .ellip {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
+5
View File
@@ -0,0 +1,5 @@
@mixin ximage {
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+11 -8
View File
@@ -3,17 +3,19 @@
<div class="a-header rounded"> <div class="a-header rounded">
<div class="art"> <div class="art">
<div <div
class="image shadow-lg" class="image shadow-lg rounded"
:style="{ :style="{
backgroundImage: `url(&quot;${imguri + props.album.image}&quot;)`, backgroundImage: `url(&quot;${imguri + props.album.image}&quot;)`,
}" }"
v-motion-slide-from-left
></div> ></div>
</div> </div>
<div class="info"> <div class="info">
<div class="top"> <div class="top" v-motion-slide-from-top>
<div <div
class="h" class="h"
v-if="props.album.artist.toLowerCase() == 'various artists'" v-if="props.album.artist.toLowerCase() == 'various artists'"
> >
Compilation Compilation
</div> </div>
@@ -23,8 +25,9 @@
<div class="bottom"> <div class="bottom">
<div class="stats"> <div class="stats">
{{ props.album.count }} Tracks {{ props.album.count }} Tracks
{{ perks.formatSeconds(props.album.duration, "long") }} {{ formatSeconds(props.album.duration, true) }}
{{ props.album.date }} {{ props.album.artist }} {{ props.album.date }}
{{ props.album.artist }}
</div> </div>
<PlayBtnRect :source="playSources.album" /> <PlayBtnRect :source="playSources.album" />
</div> </div>
@@ -34,11 +37,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import perks from "../../composables/perks.js";
import { AlbumInfo } from "../../interfaces.js";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
import { playSources } from "../../composables/enums"; import { playSources } from "../../composables/enums";
import { formatSeconds } from "../../composables/perks";
import { paths } from "../../config"; import { paths } from "../../config";
import { AlbumInfo } from "../../interfaces";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
const imguri = paths.images.thumb; const imguri = paths.images.thumb;
const props = defineProps<{ const props = defineProps<{
@@ -89,7 +92,7 @@ extrackColors();
.title { .title {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 1000; font-weight: 600;
color: white; color: white;
text-transform: capitalize; text-transform: capitalize;
} }
+13 -17
View File
@@ -1,37 +1,33 @@
<!-- <template> <template>
<div class="b-bar"> <div class="b-bar">
<div class="grid rounded"> <div class="grid rounded">
<div class="controlsx rounded"> <div class="controlsx rounded">
<div class="controls controls-bottom"> <div class="controls-bottom">
<HotKeys /> <HotKeys />
</div> </div>
<div class="progress progress-bottom"> <div class="progress progress-bottom">
<span class="durationx">{{ formatSeconds(current_pos) }}</span> <span class="durationx">{{ formatSeconds(q.track.current_time) }}</span>
<Progress /> <Progress />
<span class="durationx">{{ <span class="durationx">{{ formatSeconds(q.length) }}</span>
formatSeconds(state.current.value.length)
}}</span>
</div> </div>
<div class="r-group"> <div class="r-group">
<div id="heart" class="image ctrl-btn"></div> <div id="heart" class="image ctrl-btn"></div>
<div id="add-to" class="image ctrl-btn"></div> <div id="add-to" class="image ctrl-btn"></div>
<div id="repeat" class="image ctrl-btn"></div> <div id="repeat" class="image ctrl-btn"></div>
</div> </div>
<div class="controls controls-bottom"></div>
</div> </div>
<div class="volume-group"></div> <div class="volume-group"></div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import "../../assets/css/BottomBar/BottomBar.scss"; import "@/assets/css/BottomBar/BottomBar.scss";
import Progress from "../shared/Progress.vue"; import Progress from "../LeftSidebar/NP/Progress.vue";
import HotKeys from "../shared/HotKeys.vue"; import HotKeys from "../LeftSidebar/NP/HotKeys.vue";
import state from "../../composables/state"; import { formatSeconds } from "@/composables/perks";
import perks from "../../composables/perks";
import playAudio from "../../composables/playAudio";
const current_pos = playAudio.current_time; import useQStore from "@/stores/queue";
const formatSeconds = perks.formatSeconds;
</script> --> const q = useQStore();
</script>
+5 -1
View File
@@ -84,7 +84,7 @@ function updateQueue(track: Track) {
.table { .table {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: hidden;
.current { .current {
a { a {
@@ -141,6 +141,10 @@ function updateQueue(track: Track) {
} }
.songlist { .songlist {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
.context-on { .context-on {
background-color: $gray4; background-color: $gray4;
color: $white !important; color: $white !important;
+11 -13
View File
@@ -1,33 +1,31 @@
<template> <template>
<div class="hotkeys"> <div class="hotkeys">
<div class="image ctrl-btn" id="previous" @click="props.prev"></div> <div class="image ctrl-btn" id="previous" @click="q.playPrev"></div>
<div <div
class="image ctrl-btn play-pause" class="image ctrl-btn play-pause"
@click="playPause" @click="q.playPause"
:class="{ isPlaying: props.playing }" :class="{ isPlaying: q.playing }"
></div> ></div>
<div class="image ctrl-btn" id="next" @click="props.next"></div> <div class="image ctrl-btn" id="next" @click="q.playNext"></div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ import useQStore from "@/stores/queue";
playing: boolean;
playPause: () => void; const q = useQStore();
next: () => void;
prev: () => void;
}>();
</script> </script>
<style lang="scss"> <style lang="scss">
.hotkeys { .hotkeys {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
width: 100%;
gap: $small; gap: $small;
height: 3rem; height: 2.5rem;
align-items: center; align-items: center;
justify-content: center;
place-content: flex-end; place-content: flex-end;
width: 100%;
.ctrl-btn { .ctrl-btn {
height: 2.5rem; height: 2.5rem;
@@ -37,7 +35,7 @@ const props = defineProps<{
border-radius: 0.5rem; border-radius: 0.5rem;
&:hover { &:hover {
background-color: $red; background-color: $accent;
} }
} }
+9 -9
View File
@@ -2,7 +2,7 @@
<input <input
id="progress" id="progress"
type="range" type="range"
:value="props.pos" :value="q.track.current_time"
min="0" min="0"
max="100" max="100"
step="0.1" step="0.1"
@@ -11,13 +11,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const seek = () => { import useQStore from "../../../stores/queue";
const value = Number(document.getElementById("progress").value);
props.seek(value);
};
const props = defineProps<{ const q = useQStore();
pos: number; const seek = () => {
seek: (time: number) => void; const elem = <HTMLFormElement>document.getElementById("progress");
}>(); const value = elem.value;
q.seek(value);
};
</script> </script>
+11 -31
View File
@@ -1,11 +1,16 @@
<template> <template>
<div class="side-nav-container" :class="{ collapsed: props.collapsed }"> <div class="side-nav-container">
<router-link <router-link
v-for="menu in menus" v-for="menu in menus"
:key="menu.name" :key="menu.name"
:to="{ name: menu.route_name, params: menu.params }" :to="{ name: menu.route_name, params: menu.params }"
> >
<div class="nav-button" id="home-button"> <div
class="nav-button"
id="home-button"
v-motion-slide-from-left-100
>
<div class="in"> <div class="in">
<div class="nav-icon image" :id="`${menu.name}-icon`"></div> <div class="nav-icon image" :id="`${menu.name}-icon`"></div>
<span>{{ menu.name }}</span> <span>{{ menu.name }}</span>
@@ -15,7 +20,7 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
const menus = [ const menus = [
{ {
name: "home", name: "home",
@@ -46,36 +51,12 @@ const menus = [
route_name: "SettingsView", route_name: "SettingsView",
}, },
]; ];
const props = defineProps({
collapsed: {
type: Boolean,
default: false,
},
});
</script> </script>
<style lang="scss"> <style lang="scss">
.collapsed {
.nav-button {
margin-top: 5px;
span {
display: none;
}
.in {
width: 100%;
flex-direction: column;
}
}
}
.side-nav-container { .side-nav-container {
color: #fff; color: #fff;
margin-bottom: 1rem; margin: 1rem 0;
padding: 10px $small $small;
margin-top: 1rem;
text-transform: capitalize; text-transform: capitalize;
.nav-button { .nav-button {
@@ -83,11 +64,10 @@ const props = defineProps({
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: flex-start; justify-content: flex-start;
height: 100%; padding: 0.6rem 0;
padding: 0.6rem 0 0.6rem 0;
&:hover { &:hover {
background-color: $gray; background-color: $gray3;
} }
.nav-icon { .nav-icon {
+1 -13
View File
@@ -1,9 +1,5 @@
<template> <template>
<div <div class="side-nav-container rounded hidden" id="pinned-container">
class="side-nav-container rounded hidden"
:class="{ hidden: collapsed }"
id="pinned-container"
>
<div> <div>
<div class="nav-button" id="pinned-button"> <div class="nav-button" id="pinned-button">
<span id="text">Quick access</span> <span id="text">Quick access</span>
@@ -53,13 +49,6 @@
</div> </div>
</template> </template>
<script>
export default {
props: ["collapsed"],
setup() {},
};
</script>
<style lang="scss"> <style lang="scss">
#pinned-container { #pinned-container {
background: #6e2c00; background: #6e2c00;
@@ -102,5 +91,4 @@ export default {
#pinned-container #pinned-button:hover { #pinned-container #pinned-button:hover {
background-color: transparent; background-color: transparent;
} }
</style> </style>
+2 -4
View File
@@ -34,12 +34,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import perks from "../../composables/perks"; import { putCommas } from "../../composables/perks";
import { Track } from "../../interfaces"; import { Track } from "../../interfaces";
import { paths } from "../../config"; import { paths } from "../../config";
const imguri = paths.images.thumb const imguri = paths.images.thumb;
const putCommas = perks.putCommas;
const props = defineProps<{ const props = defineProps<{
track: Track; track: Track;
+4 -10
View File
@@ -5,18 +5,13 @@
<div class="separator no-border"></div> <div class="separator no-border"></div>
<div> <div>
<SongCard :track="queue.current" /> <SongCard :track="queue.current" />
<Progress :seek="queue.seek" :pos="queue.current_time" /> <Progress />
<HotKeys <HotKeys />
:playing="queue.playing"
:playPause="queue.playPause"
:next="queue.playNext"
:prev="queue.playPrev"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import SongCard from "./SongCard.vue"; 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";
@@ -28,9 +23,8 @@ const queue = useQStore();
.l_ { .l_ {
padding: 1rem; padding: 1rem;
background-color: $primary; background-color: $primary;
margin: $small;
text-align: center; text-align: center;
width: 14rem; width: 100%;
display: grid; display: grid;
position: relative; position: relative;
text-transform: capitalize; text-transform: capitalize;
+25
View File
@@ -0,0 +1,25 @@
<template>
<div id="logo-container"
v-motion-slide-from-top
>
<router-link :to="{ name: 'Home' }">
<div id="logo" class="rounded"></div
></router-link>
</div>
</template>
<style lang="scss">
@import "../assets/css/mixins.scss";
#logo-container {
overflow: hidden;
}
#logo {
height: 4.5rem !important;
width: 15rem;
background-image: url(./../assets/images/logo.webp);
background-size: contain;
@include ximage;
}
</style>
+3 -2
View File
@@ -28,7 +28,7 @@
<div class="type">Playlist</div> <div class="type">Playlist</div>
</div> </div>
</div> </div>
<div class="last-updated"> <div class="last-updated" v-motion-slide-from-right>
<span class="status" <span class="status"
>Last updated {{ props.info.lastUpdated }} &#160;|&#160;&#160;</span >Last updated {{ props.info.lastUpdated }} &#160;|&#160;&#160;</span
> >
@@ -46,8 +46,9 @@ import Option from "../shared/Option.vue";
import pContext from "../../contexts/playlist"; import pContext from "../../contexts/playlist";
import useContextStore from "../../stores/context"; import useContextStore from "../../stores/context";
import { paths } from "../../config"; import { paths } from "../../config";
import { onBeforeUnmount } from "vue";
const imguri = paths.images.playlist const imguri = paths.images.playlist;
const context = useContextStore(); const context = useContextStore();
const modal = useModalStore(); const modal = useModalStore();
+3 -3
View File
@@ -17,12 +17,12 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import Search from "./Search.vue"; import Search from "./Search/Main.vue";
import UpNext from "./Queue.vue"; import UpNext from "./Queue.vue";
import Main from "./Home/Main.vue"; import Main from "./Home/Main.vue";
import useTabStore from "../../stores/tabs"; import useTabStore from "../../stores/tabs";
// import Search from "./Searchh.vue";
const DashBoard = Main; const DashBoard = Main;
const tabs = useTabStore(); const tabs = useTabStore();
</script> </script>
+1 -1
View File
@@ -51,7 +51,7 @@
<script> <script>
import playAudio from "@/composables/playAudio.js"; import playAudio from "@/composables/playAudio.js";
import {ref} from "@vue/reactivity"; import {ref} from "@vue/reactivity";
import perks from "../../composables/perks.js"; import {putCommas, formatSeconds} from "../../composables/perks.js";
import HotKeys from "../shared/HotKeys.vue"; import HotKeys from "../shared/HotKeys.vue";
import Progress from "../shared/Progress.vue"; import Progress from "../shared/Progress.vue";
-189
View File
@@ -1,189 +0,0 @@
<template>
<div class="right-search">
<Options />
<div class="scrollable rounded" ref="search_thing">
<TracksGrid
v-if="tracks.tracks.length"
:more="tracks.more"
:tracks="tracks.tracks"
:query="search.query"
@loadMore="loadMoreTracks"
/>
<div class="separator no-border" v-if="tracks.tracks.length"></div>
<AlbumGrid
v-if="albums.albums.length"
:albums="albums.albums"
:more="albums.more"
@loadMore="loadMoreAlbums"
/>
<div class="separator no-border" v-if="albums.albums.length"></div>
<ArtistGrid
v-if="artists.artists.length"
:artists="artists.artists"
:more="artists.more"
@loadMore="loadMoreArtists"
/>
<div
v-if="search.query.trim().length === 0"
class="no-res border rounded"
>
<div class="no-res-text">🦋 Find your music</div>
</div>
<div
v-else-if="
!artists.artists.length &&
!tracks.tracks.length &&
!albums.albums.length
"
class="no-res border rounded"
>
<div class="no-res-text">
No results for
<span class="highlight rounded">{{ search.query }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from "@vue/reactivity";
import state from "../../composables/state";
import searchMusic from "@/composables/searchMusic.js";
import useDebouncedRef from "@/composables/useDebouncedRef";
import AlbumGrid from "@/components/Search/AlbumGrid.vue";
import ArtistGrid from "@/components/Search/ArtistGrid.vue";
import TracksGrid from "@/components/Search/TracksGrid.vue";
import Options from "@/components/Search/Options.vue";
import loadMore from "../../composables/loadmore";
import useSearchStore from "../../stores/gsearch";
import useTabStore from "../../stores/tabs";
import "@/assets/css/Search/Search.scss";
const search = useSearchStore();
const tabs = useTabStore();
const search_thing = ref(null);
const tracks = reactive({
tracks: [],
more: false,
});
let albums = reactive({
albums: [],
more: false,
});
const artists = reactive({
artists: [],
more: false,
});
function scrollSearchThing() {
search_thing.value.scroll({
top: search_thing.value.scrollTop + 330,
left: 0,
behavior: "smooth",
});
}
function loadMoreTracks(start) {
scrollSearchThing();
loadMore.loadMoreTracks(start).then((response) => {
tracks.tracks = [...tracks.tracks, ...response.tracks];
tracks.more = response.more;
});
}
function loadMoreAlbums(start) {
loadMore.loadMoreAlbums(start).then((response) => {
albums.albums = [...albums.albums, ...response.albums];
albums.more = response.more;
});
}
function loadMoreArtists(start) {
scrollSearchThing();
loadMore.loadMoreArtists(start).then((response) => {
artists.artists = [...artists.artists, ...response.artists];
artists.more = response.more;
});
}
search.$subscribe((mutation, state) => {
if (state.query.trim() == "") {
tracks.tracks = [];
albums.albums = [];
artists.artists = [];
return;
}
searchMusic(state.query).then((res) => {
if (tabs.current !== tabs.tabs.search) {
tabs.switchToSearch();
}
albums.albums = res.albums.albums;
albums.more = res.albums.more;
artists.artists = res.artists.artists;
artists.more = res.artists.more;
tracks.tracks = res.tracks.tracks;
tracks.more = res.tracks.more;
});
});
</script>
<style lang="scss">
.right-search {
position: relative;
display: grid;
grid-template-rows: min-content 1fr;
overflow: hidden;
width: auto;
height: 100%;
padding: $small $small 0 0;
.no-res {
text-align: center;
display: grid;
height: calc(100% - $small);
place-items: center;
font-size: 2rem;
transition: all 0.3s ease;
line-height: 4rem !important;
.highlight {
padding: $small;
background-color: rgb(29, 26, 26);
}
}
.heading {
padding: $medium;
border-radius: $small;
margin-bottom: $small;
font-size: 2rem;
color: $white;
}
.input {
display: flex;
align-items: center;
position: relative;
}
}
.right-search .scrollable {
overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
</style>
@@ -1,36 +1,29 @@
<template> <template>
<div class="albums-results border"> <div class="albums-results border">
<div class="heading">Albums</div>
<div class="grid"> <div class="grid">
<AlbumCard v-for="album in albums" :key="album" :album="album" /> <AlbumCard
v-for="album in search.albums.value"
:key="album.image"
:album="album"
/>
</div> </div>
<LoadMore v-if="more" @loadMore="loadMore" /> <LoadMore v-if="search.albums.more" @loadMore="loadMore()" />
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import AlbumCard from "@/components/shared/AlbumCard.vue"; import AlbumCard from "../../shared/AlbumCard.vue";
import LoadMore from "./LoadMore.vue"; import LoadMore from "./LoadMore.vue";
import useSearchStore from "../../../stores/search";
export default { const search = useSearchStore();
props: ["albums", "more"],
components: {
AlbumCard,
LoadMore,
},
setup(props, { emit }) {
let counter = 0;
function loadMore() { let counter = 0;
function loadMore() {
counter += 6; counter += 6;
emit("loadMore", counter); search.loadAlbums(counter);
} }
return {
loadMore,
};
},
};
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -0,0 +1,45 @@
<template>
<div class="artists-results border">
<div class="grid">
<ArtistCard
v-for="artist in search.artists.value"
:key="artist.image"
:artist="artist"
/>
</div>
<LoadMore v-if="search.artists.more" @loadMore="loadMore" />
</div>
</template>
<script setup lang="ts">
import ArtistCard from "../../shared/ArtistCard.vue";
import LoadMore from "./LoadMore.vue";
import useSearchStore from "../../../stores/search";
const search = useSearchStore();
let counter = 0;
function loadMore() {
counter += 6;
search.loadArtists(counter);
}
</script>
<style lang="scss">
.right-search .artists-results {
border-radius: 0.5rem;
padding: $small;
margin-bottom: $small;
.xartist {
background-color: $gray;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
}
</style>
@@ -0,0 +1,75 @@
<template>
<div class="right-search">
<TabsWrapper>
<Tab name="tracks">
<TracksGrid />
</Tab>
<Tab name="albums">
<AlbumGrid />
</Tab>
<Tab name="artists">
<ArtistGrid />
</Tab>
</TabsWrapper>
<component :is="s.currentTab" />
</div>
</template>
<script setup lang="ts">
import TabsWrapper from "./TabsWrapper.vue";
import Tab from "./Tab.vue";
import TracksGrid from "./TracksGrid.vue";
import AlbumGrid from "./AlbumGrid.vue";
import ArtistGrid from "./ArtistGrid.vue";
import "@/assets/css/Search/Search.scss";
import useSearchStore from "@/stores/search";
const s = useSearchStore();
</script>
<style lang="scss">
.right-search {
position: relative;
overflow: hidden;
width: auto;
height: 100%;
.no-res {
text-align: center;
display: grid;
height: calc(100% - $small);
place-items: center;
font-size: 2rem;
transition: all 0.3s ease;
line-height: 4rem !important;
.highlight {
padding: $small;
background-color: rgb(29, 26, 26);
}
}
.heading {
padding: $medium;
border-radius: $small;
margin-bottom: $small;
font-size: 2rem;
color: $white;
}
.input {
display: flex;
align-items: center;
position: relative;
}
}
.right-search .scrollable {
overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
</style>
@@ -0,0 +1,13 @@
<template>
<div v-show="name == s.currentTab" v-motion-slide-visible-top>
<slot />
</div>
</template>
<script setup lang="ts">
import useSearchStore from "@/stores/search";
const s = useSearchStore();
defineProps<{
name: string;
}>();
</script>
@@ -0,0 +1,65 @@
<template>
<div id="right-tabs">
<div id="tabheaders">
<div
class="tab rounded"
v-for="slot in $slots.default()"
:key="slot.key"
@click="s.changeTab(slot.props.name)"
:class="{ activetab: slot.props.name === s.currentTab }"
>
{{ slot.props.name }}
</div>
</div>
<div id="tab-content">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import useSearchStore from "@/stores/search";
const s = useSearchStore();
</script>
<style lang="scss">
#right-tabs {
height: 100%;
margin-right: $small;
display: grid;
grid-template-rows: min-content 1fr;
#tabheaders {
display: flex;
gap: $small;
margin: $small 0;
.tab {
background-color: $gray3;
padding: $small;
text-transform: capitalize;
cursor: pointer;
display: flex;
justify-content: center;
transition: all 0.3s ease;
width: 4rem;
}
.activetab {
background-color: $accent;
width: 6rem;
transition: all 0.3s ease;
}
}
#tab-content {
height: 100%;
overflow: auto;
overflow-x: hidden;
border-radius: $small;
background-color: $gray;
// overflow: hidden;
}
}
</style>
@@ -1,9 +1,8 @@
<template> <template>
<div class="tracks-results border" v-if="tracks"> <div id="tracks-results" v-if="search.tracks.value">
<div class="heading">Tracks</div> <TransitionGroup name="list">
<TransitionGroup class="items" name="list">
<TrackItem <TrackItem
v-for="track in tracks" v-for="track in search.tracks.value"
:key="track.trackid" :key="track.trackid"
:track="track" :track="track"
:isPlaying="queue.playing" :isPlaying="queue.playing"
@@ -12,43 +11,38 @@
@PlayThis="updateQueue" @PlayThis="updateQueue"
/> />
</TransitionGroup> </TransitionGroup>
<LoadMore v-if="more" @loadMore="loadMore" /> <LoadMore v-if="search.tracks.more" @loadMore="loadMore" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import LoadMore from "./LoadMore.vue"; import LoadMore from "./LoadMore.vue";
import TrackItem from "../shared/TrackItem.vue"; import TrackItem from "../../shared/TrackItem.vue";
import useQStore from "../../stores/queue"; import useQStore from "../../../stores/queue";
import { Track } from "../../interfaces"; import { Track } from "../../../interfaces";
import useSearchStore from "../../../stores/search";
let counter = 0; let counter = 0;
const queue = useQStore(); const queue = useQStore();
const search = useSearchStore();
const props = defineProps<{
tracks: Track[];
more: boolean;
query: string;
}>();
const emit = defineEmits(["loadMore"]);
function loadMore() { function loadMore() {
counter += 5; counter += 5;
emit("loadMore", counter); search.loadTracks(counter);
} }
function updateQueue(track: Track) { function updateQueue(track: Track) {
console.log(props.query); queue.playFromSearch(search.query, search.tracks.value);
queue.playFromSearch(props.query, props.tracks);
queue.play(track); queue.play(track);
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.right-search .tracks-results { .right-search #tracks-results {
border-radius: 0.5rem; border-radius: 0.5rem;
padding: $small; padding: $small;
height: 100% !important;
overflow: hidden;
.list-enter-active, .list-enter-active,
.list-leave-active { .list-leave-active {
+26 -33
View File
@@ -1,47 +1,36 @@
<template> <template>
<div class="gsearch-input"> <div id="gsearch-input">
<Filters :filters="search.filters" @removeFilter="removeFilter" /> <div id="ginner" tabindex="0">
<div class="input-loader"> <div class="icon image"></div>
<input <input
id="search" id="search"
class="rounded" class="rounded"
v-model="search.query" v-model="search.query"
placeholder="Search your library" placeholder="Search your library"
type="text" type="text"
@keyup.backspace="removeLastFilter" @focus="focusThis()"
@blur="unfocusThis()"
/> />
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import Filters from "../Search/Filters.vue"; import useSearchStore from "../../stores/search";
import Loader from "../shared/Loader.vue";
import useSearchStore from "../../stores/gsearch";
const search = useSearchStore(); const search = useSearchStore();
function removeFilter(filter) { function focusThis() {
search.removeFilter(filter); document.getElementById("ginner").classList.add("focused");
} }
let counter = 0; function unfocusThis() {
document.getElementById("ginner").classList.remove("focused");
function removeLastFilter() {
if (search.query === "") {
counter++;
if (counter > 0) {
search.removeLastFilter();
}
} else {
counter = 0;
}
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.gsearch-input { #gsearch-input {
padding: $small; padding: $small;
display: flex; display: flex;
@@ -49,15 +38,20 @@ function removeLastFilter() {
display: none; display: none;
} }
.input-loader { #ginner {
width: 100%; width: 100%;
border-radius: 0.4rem; border-radius: 0.4rem;
position: relative; position: relative;
display: flex;
gap: $small;
background-color: $gray4;
height: 2.25rem;
._loader { .icon {
position: absolute; width: 2rem;
top: -0.15rem; background-image: url("../../assets/icons/search.svg");
right: 2rem; background-size: 1.5rem;
margin-left: $smaller;
} }
input { input {
@@ -66,16 +60,15 @@ function removeLastFilter() {
width: 100%; width: 100%;
border: none; border: none;
line-height: 2.25rem; line-height: 2.25rem;
background-color: $black;
color: inherit; color: inherit;
font-size: 1rem; font-size: 1rem;
padding-left: 0.75rem; background-color: transparent;
outline: 2px solid transparent; outline: 2px solid transparent;
}
}
&:focus { .focused {
outline: solid $accent; outline: solid $accent;
} }
}
}
} }
</style> </style>
+2 -2
View File
@@ -12,7 +12,7 @@
<p class="title ellip">{{ next.title }}</p> <p class="title ellip">{{ next.title }}</p>
<hr /> <hr />
<p class="artist ellip"> <p class="artist ellip">
<span v-for="artist in perks.putCommas(next.artists)" :key="artist">{{ <span v-for="artist in putCommas(next.artists)" :key="artist">{{
artist artist
}}</span> }}</span>
</p> </p>
@@ -23,7 +23,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Track } from "../../../interfaces"; import { Track } from "../../../interfaces";
import perks from "../../../composables/perks"; import {putCommas} from "../../../composables/perks";
import { paths } from "../../../config"; import { paths } from "../../../config";
const imguri = paths.images.thumb; const imguri = paths.images.thumb;
-52
View File
@@ -1,52 +0,0 @@
<template>
<div class="artists-results border">
<div class="heading">Artists</div>
<div class="grid">
<ArtistCard v-for="artist in artists" :key="artist" :artist="artist" />
</div>
<LoadMore v-if="more" @loadMore="loadMore" />
</div>
</template>
<script>
import ArtistCard from "@/components/shared/ArtistCard.vue";
import LoadMore from "./LoadMore.vue";
export default {
props: ["artists", "more"],
components: {
ArtistCard,
LoadMore,
},
setup(props, { emit }) {
let counter = 0;
function loadMore() {
counter += 6;
emit("loadMore", counter);
}
return {
loadMore,
};
},
};
</script>
<style lang="scss">
.right-search .artists-results {
border-radius: 0.5rem;
padding: $small;
margin-bottom: $small;
.xartist {
background-color: $gray;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
}
</style>
-52
View File
@@ -1,52 +0,0 @@
<template>
<div class="filter">
<div
class="item"
v-for="filter in filters"
:key="filter"
@click="removeFilter(filter)"
>
{{ filter }}<span class="cancel image"></span>
</div>
</div>
</template>
<script>
export default {
props: ["filters"],
setup(props, { emit }) {
const removeFilter = (filter) => {
emit("removeFilter", filter);
};
return {
removeFilter,
};
},
};
</script>
<style lang="scss">
.gsearch-input .filter {
display: flex;
.item {
transition: all 0.2s ease-in-out;
background-color: $gray3;
height: 2.5rem;
&:hover {
width: 4rem;
.cancel {
position: absolute;
right: 0.5rem;
width: 1.5rem;
height: 1.5rem;
background-image: url(../../assets/icons/a.svg);
background-size: 70%;
}
}
}
}
</style>
-90
View File
@@ -1,90 +0,0 @@
<template>
<div class="options border rounded">
<div class="item info header">Filter by:</div>
<div
class="item"
v-for="option in options"
:key="option"
@click="search.addFilter(option.icon)"
>
<div>
<span class="icon">{{ option.icon }}</span>
<span class="title">&nbsp;&nbsp;{{ option.title }}</span>
</div>
</div>
</div>
</template>
<script setup>
import useSearchStore from "../../stores/gsearch";
const search = useSearchStore();
const options = [
{
title: "Track",
icon: "🎵",
},
{
title: "Album",
icon: "💿",
},
{
title: "Artist",
icon: "👤",
},
{
title: "Playlist",
icon: "🎧",
},
{
title: "Folder",
icon: "📁",
},
];
</script>
<style lang="scss">
.right-search .options {
display: flex;
margin-bottom: $small;
.item {
margin: $small;
width: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: all 0.2s ease-in-out;
position: relative;
background-color: $gray3;
.title {
position: absolute;
left: 1.5rem;
top: 0.5rem;
visibility: hidden;
}
.icon {
position: absolute;
top: 0.5rem;
left: 0.75rem;
}
&:hover {
width: 5.5rem;
background-color: $gray5;
.title {
visibility: visible;
}
}
}
.header {
width: 5.5rem;
}
}
</style>
+37 -4
View File
@@ -6,12 +6,45 @@
</div> </div>
<div class="info"> <div class="info">
<div class="title" v-if="$route.name == 'Playlists'">Playlists</div> <div
<div class="folder" v-else-if="$route.name == 'FolderView'"> class="title"
v-show="$route.name == 'Playlists'"
v-motion
:initial="{
opacity: 0,
x: -20,
}"
:visible="{
opacity: 1,
x: 0,
transition: {
delay: 100,
},
}"
>
Playlists
</div>
<div
class="folder"
v-show="$route.name == 'FolderView'"
v-motion
:initial="{
opacity: 0,
x: -20,
}"
:visible="{
opacity: 1,
x: 0,
transition: {
delay: 100,
},
}"
>
<div class="fname"> <div class="fname">
<div class="icon image"></div> <div class="icon image"></div>
<div class="ellip"> <div class="ellip">
{{ $route.params.path.split("/").splice(-1)[0] }} <!-- {{ $route.params.path.split("/").splice(-1)[0] }} -->
{{ $route.params.path }}
</div> </div>
</div> </div>
</div> </div>
@@ -28,7 +61,7 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import NavButtons from "./NavButtons.vue"; import NavButtons from "./NavButtons.vue";
import Loader from "../shared/Loader.vue"; import Loader from "../shared/Loader.vue";
import Search from "./Search.vue"; import Search from "./Search.vue";
-4
View File
@@ -5,10 +5,6 @@
</div> </div>
</template> </template>
<script setup>
console.log();
</script>
<style lang="scss"> <style lang="scss">
#back-forward { #back-forward {
display: grid; display: grid;
+1 -1
View File
@@ -5,7 +5,7 @@
type="search" type="search"
name="" name=""
id="" id=""
placeholder="Search this playlist" placeholder="Search here"
class="rounded" class="rounded"
/> />
</form> </form>
+1 -1
View File
@@ -17,7 +17,7 @@ const imguri = paths.images.artist;
defineProps<{ defineProps<{
artist: any; artist: any;
color: string; color?: string;
}>(); }>();
</script> </script>
+1 -1
View File
@@ -30,7 +30,7 @@ function play() {
queue.play(queue.tracks[0]); queue.play(queue.tracks[0]);
break; break;
case playSources.album: case playSources.album:
queue.playFromAlbum(album.info.album, album.info.artist, album.tracks); queue.playFromAlbum(album.info.title, album.info.artist, album.tracks);
queue.play(album.tracks[0]); queue.play(album.tracks[0]);
break; break;
case playSources.playlist: case playSources.playlist:
+6 -8
View File
@@ -10,9 +10,7 @@
<div <div
class="album-art image rounded" class="album-art image rounded"
:style="{ :style="{
backgroundImage: `url(&quot;${ backgroundImage: `url(&quot;${imguri + props.song.image}&quot;`,
imguri + props.song.image
}&quot;`,
}" }"
@click="emitUpdate(props.song)" @click="emitUpdate(props.song)"
> >
@@ -30,7 +28,7 @@
<div class="ellip" v-if="props.song.artists[0] !== ''"> <div class="ellip" v-if="props.song.artists[0] !== ''">
<span <span
class="artist" class="artist"
v-for="artist in perks.putCommas(props.song.artists)" v-for="artist in putCommas(props.song.artists)"
:key="artist" :key="artist"
>{{ artist }}</span >{{ artist }}</span
> >
@@ -54,13 +52,13 @@
</div> </div>
</router-link> </router-link>
<div class="song-duration"> <div class="song-duration">
{{ perks.formatSeconds(props.song.length) }} {{ formatSeconds(props.song.length) }}
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import perks from "../../composables/perks.js"; 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";
@@ -68,13 +66,13 @@ import { ContextSrc } from "../../composables/enums";
import { ref } from "vue"; import { ref } from "vue";
import trackContext from "../../contexts/track_context"; import trackContext from "../../contexts/track_context";
import { Track } from "../../interfaces.js"; import { Track } from "../../interfaces";
import { paths } from "../../config"; import { paths } from "../../config";
const contextStore = useContextStore(); const contextStore = useContextStore();
const context_on = ref(false); const context_on = ref(false);
const imguri = paths.images.thumb const imguri = paths.images.thumb;
const showContextMenu = (e: Event) => { const showContextMenu = (e: Event) => {
e.preventDefault(); e.preventDefault();
+2 -7
View File
@@ -36,7 +36,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import perks from "../../composables/perks"; import { putCommas } from "../../composables/perks";
import trackContext from "../../contexts/track_context"; import trackContext from "../../contexts/track_context";
import { Track } from "../../interfaces"; import { Track } from "../../interfaces";
import { ContextSrc } from "../../composables/enums"; import { ContextSrc } from "../../composables/enums";
@@ -46,9 +46,8 @@ import useModalStore from "../../stores/modal";
import useQueueStore from "../../stores/queue"; import useQueueStore from "../../stores/queue";
import { paths } from "../../config"; import { paths } from "../../config";
const contextStore = useContextStore(); const contextStore = useContextStore();
const imguri = paths.images.thumb const imguri = paths.images.thumb;
const props = defineProps<{ const props = defineProps<{
track: Track; track: Track;
@@ -78,9 +77,6 @@ const emit = defineEmits<{
(e: "PlayThis", track: Track): void; (e: "PlayThis", track: Track): void;
}>(); }>();
const current = ref(perks.current);
const putCommas = perks.putCommas;
const playThis = (track: Track) => { const playThis = (track: Track) => {
emit("PlayThis", track); emit("PlayThis", track);
}; };
@@ -97,7 +93,6 @@ const playThis = (track: Track) => {
} }
.track-item { .track-item {
width: 26.55rem;
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 0.5rem; border-radius: 0.5rem;
+5 -1
View File
@@ -64,11 +64,15 @@ export default function (queue: any) {
if (!key_down_fired) { if (!key_down_fired) {
if (!ctrlKey) return; if (!ctrlKey) return;
e.preventDefault(); e.preventDefault();
focusSearchBox();
key_down_fired = true; key_down_fired = true;
} }
} }
case "/": {{
e.preventDefault();
focusSearchBox();
}}
} }
}); });
} }
-42
View File
@@ -1,42 +0,0 @@
import axios from "axios";
const url = "http://127.0.0.1:9876/search/loadmore";
async function loadMoreTracks(start) {
const response = await axios.get(url, {
params: {
type: "tracks",
start: start,
},
});
return response.data;
}
async function loadMoreAlbums(start) {
const response = await axios.get(url, {
params: {
type: "albums",
start: start,
},
});
return response.data;
}
async function loadMoreArtists(start) {
const response = await axios.get(url, {
params: {
type: "artists",
start: start,
},
});
return response.data;
}
export default {
loadMoreTracks,
loadMoreAlbums,
loadMoreArtists,
};
+5 -4
View File
@@ -1,9 +1,10 @@
import perks from "./perks"; import { getElem } from "./perks";
export default (mouseX, mouseY) => { export default (mouseX, mouseY) => {
const scope = perks.getElem("app", "id"); const scope = getElem("app", "id");
const contextMenu = perks.getElem("context-menu", "class"); const contextMenu = getElem("context-menu", "class");
// ? compute what is the mouse position relative to the container element (scope) // ? compute what is the mouse position relative to the container element
// (scope)
let { left: scopeOffsetX, top: scopeOffsetY } = scope.getBoundingClientRect(); let { left: scopeOffsetX, top: scopeOffsetY } = scope.getBoundingClientRect();
scopeOffsetX = scopeOffsetX < 0 ? 0 : scopeOffsetX; scopeOffsetX = scopeOffsetX < 0 ? 0 : scopeOffsetX;
@@ -1,4 +1,4 @@
const putCommas = (artists) => { const putCommas = (artists: string[]) => {
let result = []; let result = [];
artists.forEach((i, index, artists) => { artists.forEach((i, index, artists) => {
@@ -24,18 +24,18 @@ function focusCurrent() {
} }
} }
function getElem(identifier, type) { function getElem(id: string, type: string) {
switch (type) { switch (type) {
case "class": { case "class": {
return document.getElementsByClassName(identifier)[0]; return document.getElementsByClassName(id)[0];
} }
case "id": { case "id": {
return document.getElementById(identifier); return document.getElementById(id);
} }
} }
} }
function formatSeconds(seconds) { function formatSeconds(seconds: number, long?: boolean) {
// check if there are arguments // check if there are arguments
const date = new Date(seconds * 1000); const date = new Date(seconds * 1000);
@@ -48,7 +48,7 @@ function formatSeconds(seconds) {
let _mm = mm < 10 ? `0${mm}` : mm; let _mm = mm < 10 ? `0${mm}` : mm;
let _ss = ss < 10 ? `0${ss}` : ss; let _ss = ss < 10 ? `0${ss}` : ss;
if (arguments[1]) { if (long == true) {
if (hh === 1) { if (hh === 1) {
_hh = hh + " Hour"; _hh = hh + " Hour";
} else { } else {
@@ -75,10 +75,4 @@ function formatSeconds(seconds) {
} }
} }
export { putCommas, focusCurrent, formatSeconds, getElem };
export default {
putCommas,
focusCurrent,
formatSeconds,
getElem,
};
-27
View File
@@ -1,27 +0,0 @@
import state from "./state";
const base_url = `${state.settings.uri}/search?q=`;
async function search(query) {
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],
};
}
export default search;
+106
View File
@@ -0,0 +1,106 @@
import state from "./state";
import axios from "axios";
const base_url = `${state.settings.uri}/search`;
const uris = {
tracks: `${base_url}/tracks?q=`,
albums: `${base_url}/albums?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) {
const url = uris.tracks + 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();
return data;
}
async function searchAlbums(query: string) {
const url = uris.albums + encodeURIComponent(query.trim());
const res = await axios.get(url);
return res.data;
}
async function searchArtists(query: string) {
const url = uris.artists + encodeURIComponent(query.trim());
const res = await axios.get(url);
return res.data;
}
const url = state.settings.uri + "/search/loadmore";
async function loadMoreTracks(index: number) {
const response = await axios.get(url, {
params: {
type: "tracks",
index: index,
},
});
return response.data;
}
async function loadMoreAlbums(index: number) {
const response = await axios.get(url, {
params: {
type: "albums",
index: index,
},
});
return response.data;
}
async function loadMoreArtists(index: number) {
const response = await axios.get(url, {
params: {
type: "artists",
index: index,
},
});
return response.data;
}
export {
searchTracks,
searchAlbums,
searchArtists,
loadMoreTracks,
loadMoreAlbums,
loadMoreArtists,
};
+34 -18
View File
@@ -1,33 +1,49 @@
import {customRef, ref} from 'vue' import { customRef, ref } from "vue";
/**
* Debounces a function
*
* @param {*} fn The function to debounce
* @param {*} delay The delay in milliseconds
* @param {*} immediate whether to debounce immediately
* @returns {Function} The debounced function
*/
const debounce = (fn, delay = 0, immediate = false) => { const debounce = (fn, delay = 0, immediate = false) => {
let timeout let timeout;
return (...args) => { return (...args) => {
if (immediate && !timeout) fn(...args) if (immediate && !timeout) fn(...args);
clearTimeout(timeout) clearTimeout(timeout);
timeout = setTimeout(() => { timeout = setTimeout(() => {
fn(...args) fn(...args);
}, delay) }, delay);
} };
} };
const useDebouncedRef = (initialValue, delay, immediate) => { /**
const state = ref(initialValue) * Emits the ref updated value after the given delay.
*
* @param {*} initialValue The default value of the ref
* @param {*} delay The delay in milliseconds
* @param {*} immediate Whether to call the function immediately
* @returns {Object} The ref and a function to call to update the ref
*/
const useDebouncedRef = (initialValue, delay, immediate = false) => {
const state = ref(initialValue);
return customRef((track, trigger) => ({ return customRef((track, trigger) => ({
get() { get() {
track() track();
return state.value return state.value;
}, },
set: debounce( set: debounce(
value => { (value) => {
state.value = value state.value = value;
trigger() trigger();
}, },
delay, delay,
immediate immediate
), ),
})) }));
} };
export default useDebouncedRef export default useDebouncedRef;
+5 -5
View File
@@ -137,14 +137,14 @@ export default async (
add_to_playlist, add_to_playlist,
play_next, play_next,
add_to_q, add_to_q,
add_to_fav, // add_to_fav,
separator, separator,
go_to_folder, go_to_folder,
go_to_artist, // go_to_artist,
go_to_alb_artist, // go_to_alb_artist,
go_to_album, go_to_album,
separator, // separator,
del_track, // del_track,
]; ];
return options; return options;
+13 -8
View File
@@ -1,13 +1,18 @@
import { createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker"; import "./registerServiceWorker";
import router from "./router";
import { createPinia } from 'pinia'
import "../src/assets/css/global.scss"; import "../src/assets/css/global.scss";
import { MotionPlugin } from "@vueuse/motion";
import { createPinia } from "pinia";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import useCustomTransitions from "./transitions";
const app = createApp(App); const app = createApp(App);
app.use(createPinia())
app.use(createPinia());
app.use(router); app.use(router);
app.mount('#app'); app.use(MotionPlugin, useCustomTransitions);
app.mount("#app");
-67
View File
@@ -1,67 +0,0 @@
import { defineStore } from "pinia";
import useDebouncedRef from "../composables/useDebouncedRef";
export default defineStore("gsearch", {
state: () => ({
filters: [],
query: useDebouncedRef("", 600),
results: {
tracks: {
items: [],
more: false,
},
albums: {
items: [],
more: false,
},
artists: {
items: [],
more: false,
},
},
}),
actions: {
addFilter(filter) {
if (this.filters.includes(filter)) {
return;
}
this.filters.push(filter);
},
removeFilter(filter) {
this.filters = this.filters.filter((f) => f !== filter);
},
removeLastFilter() {
this.filters.pop();
},
updateQuery(query) {
this.query = query;
},
updateTrackResults(results) {
this.results.tracks = results;
},
addMoreTrackResults(results) {
this.results.tracks.items = [
...this.results.tracks.items,
...results.items,
];
},
updateAlbumResults(results) {
this.results.albums = results;
},
addMoreAlbumResults(results) {
this.results.albums.items = [
...this.results.albums.items,
...results.items,
];
},
updateArtistResults(results) {
this.results.artists = results;
},
addMoreArtistResults(results) {
this.results.artists.items = [
...this.results.artists.items,
...results.items,
];
},
},
});
-1
View File
@@ -12,7 +12,6 @@ export default defineStore("Loader", {
}, },
stopLoading() { stopLoading() {
const diff = new Date().getTime() - this.duration; const diff = new Date().getTime() - this.duration;
console.log(diff);
if (diff <= 250) { if (diff <= 250) {
setTimeout(() => { setTimeout(() => {
+25 -13
View File
@@ -12,7 +12,7 @@ import {
import notif from "../composables/mediaNotification"; import notif from "../composables/mediaNotification";
import { FromOptions } from "../composables/enums"; import { FromOptions } from "../composables/enums";
function addQToLocalStorage( function writeQueue(
from: fromFolder | fromAlbum | fromPlaylist, from: fromFolder | fromAlbum | fromPlaylist,
tracks: Track[] tracks: Track[]
) { ) {
@@ -25,11 +25,11 @@ function addQToLocalStorage(
); );
} }
function addCurrentToLocalStorage(track: Track) { function writeCurrent(track: Track) {
localStorage.setItem("current", JSON.stringify(track)); localStorage.setItem("current", JSON.stringify(track));
} }
function readCurrentFromLocalStorage(): Track { function readCurrent(): Track {
const current = localStorage.getItem("current"); const current = localStorage.getItem("current");
if (current) { if (current) {
return JSON.parse(current); return JSON.parse(current);
@@ -48,11 +48,14 @@ export default defineStore("Queue", {
state: () => ({ state: () => ({
progressElem: HTMLElement, progressElem: HTMLElement,
audio: new Audio(), audio: new Audio(),
track: {
current_time: 0,
duration: 0,
},
current: <Track>{}, current: <Track>{},
next: <Track>{}, next: <Track>{},
prev: <Track>{}, prev: <Track>{},
playing: false, playing: false,
current_time: 0,
from: <fromFolder>{} || <fromAlbum>{} || <fromPlaylist>{}, from: <fromFolder>{} || <fromAlbum>{} || <fromPlaylist>{},
tracks: <Track[]>[defaultTrack], tracks: <Track[]>[defaultTrack],
}), }),
@@ -68,14 +71,16 @@ export default defineStore("Queue", {
this.audio.onerror = reject; this.audio.onerror = reject;
}) })
.then(() => { .then(() => {
this.track.duration = this.audio.duration;
this.audio.play().then(() => { this.audio.play().then(() => {
this.playing = true; this.playing = true;
notif(track, this.playPause, this.playNext, this.playPrev); notif(track, this.playPause, this.playNext, this.playPrev);
this.audio.ontimeupdate = () => { this.audio.ontimeupdate = () => {
this.current_time = this.track.current_time =
(this.audio.currentTime / this.audio.duration) * 100; (this.audio.currentTime / this.audio.duration) * 100;
elem.style.backgroundSize = `${this.current_time}% 100%`; elem.style.backgroundSize = `${this.track.current_time}% 100%`;
}; };
this.audio.onended = () => { this.audio.onended = () => {
@@ -114,7 +119,7 @@ export default defineStore("Queue", {
} }
} }
}, },
readQueueFromLocalStorage() { readQueue() {
const queue = localStorage.getItem("queue"); const queue = localStorage.getItem("queue");
if (queue) { if (queue) {
@@ -123,7 +128,7 @@ export default defineStore("Queue", {
this.tracks = parsed.tracks; this.tracks = parsed.tracks;
} }
this.updateCurrent(readCurrentFromLocalStorage()); this.updateCurrent(readCurrent());
}, },
updateCurrent(track: Track) { updateCurrent(track: Track) {
this.current = track; this.current = track;
@@ -131,7 +136,7 @@ export default defineStore("Queue", {
this.updateNext(this.current); this.updateNext(this.current);
this.updatePrev(this.current); this.updatePrev(this.current);
addCurrentToLocalStorage(track); writeCurrent(track);
}, },
updateNext(track: Track) { updateNext(track: Track) {
const index = this.tracks.findIndex( const index = this.tracks.findIndex(
@@ -161,8 +166,9 @@ export default defineStore("Queue", {
}, },
setNewQueue(tracklist: Track[]) { setNewQueue(tracklist: Track[]) {
if (this.tracks !== tracklist) { if (this.tracks !== tracklist) {
this.tracks = tracklist; this.tracks = [];
addQToLocalStorage(this.from, this.tracks); this.tracks.push(...tracklist);
writeQueue(this.from, this.tracks);
} }
}, },
playFromFolder(fpath: string, tracks: Track[]) { playFromFolder(fpath: string, tracks: Track[]) {
@@ -201,7 +207,8 @@ export default defineStore("Queue", {
}, },
addTrackToQueue(track: Track) { addTrackToQueue(track: Track) {
this.tracks.push(track); this.tracks.push(track);
addQToLocalStorage(this.from, this.tracks); writeQueue(this.from, this.tracks);
this.updateNext(this.current);
}, },
playTrackNext(track: Track) { playTrackNext(track: Track) {
const Toast = useNotifStore(); const Toast = useNotifStore();
@@ -209,19 +216,24 @@ export default defineStore("Queue", {
(t: Track) => t.trackid === this.current.trackid (t: Track) => t.trackid === this.current.trackid
); );
if (currentid == this.tracks.length - 1) {
this.tracks.push(track);
} else {
const next: Track = this.tracks[currentid + 1]; const next: Track = this.tracks[currentid + 1];
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);
return; return;
} }
}
this.tracks.splice(currentid + 1, 0, track); this.tracks.splice(currentid + 1, 0, track);
this.updateNext(this.current);
Toast.showNotification( Toast.showNotification(
`Added ${track.title} to queue`, `Added ${track.title} to queue`,
NotifType.Success NotifType.Success
); );
addQToLocalStorage(this.from, this.tracks); writeQueue(this.from, this.tracks);
}, },
}, },
}); });
+185
View File
@@ -0,0 +1,185 @@
import { ref, reactive } from "@vue/reactivity";
import { defineStore } from "pinia";
import { AlbumInfo, Artist, Track } from "../interfaces";
import {
searchTracks,
searchAlbums,
searchArtists,
loadMoreTracks,
loadMoreAlbums,
loadMoreArtists,
} from "../composables/searchMusic";
import { watch } from "vue";
import useDebouncedRef from "../composables/useDebouncedRef";
import useTabStore from "./tabs";
/**
*
* @param id The id of the element of the div to scroll
* Scrolls on clicking the loadmore button
*/
function scrollOnLoad(id: string) {
const elem = document.getElementById(id);
elem.scroll({
top: elem.scrollHeight,
left: 0,
behavior: "smooth",
});
}
export default defineStore("search", () => {
const query = useDebouncedRef(null, 600);
const currentTab = ref("tracks");
const tracks = reactive({
value: <Track[]>[],
more: false,
});
const albums = reactive({
value: <AlbumInfo[]>[],
more: false,
});
const artists = reactive({
value: <Artist[]>[],
more: false,
});
/**
* Searches for tracks, albums and artists
* @param query query to search for
*/
function fetchTracks(query: string) {
searchTracks(query).then((res) => {
tracks.value = res.tracks;
tracks.more = res.more;
});
}
function fetchAlbums(query: string) {
searchAlbums(query).then((res) => {
albums.value = res.albums;
albums.more = res.more;
});
}
function fetchArtists(query: string) {
searchArtists(query).then((res) => {
artists.value = res.artists;
artists.more = res.more;
});
}
/**
* Loads more search tracks results
*
* @param index The starting index of the tracks to load
*/
function loadTracks(index: number) {
loadMoreTracks(index)
.then((res) => {
tracks.value = [...tracks.value, ...res.tracks];
tracks.more = res.more;
})
.then(() => {
scrollOnLoad("tab-content");
});
}
/**
* Loads more search albums results
*
* @param index The starting index of the albums to load
*/
function loadAlbums(index: number) {
loadMoreAlbums(index)
.then((res) => {
albums.value = [...albums.value, ...res.albums];
albums.more = res.more;
})
.then(() => {
scrollOnLoad("tab-content");
});
}
/**
* Loads more search artists results
*
* @param index The starting index of the artists to load
*/
function loadArtists(index: number) {
loadMoreArtists(index)
.then((res) => {
artists.value = [...artists.value, ...res.artists];
artists.more = res.more;
})
.then(() => {
scrollOnLoad("tab-content");
});
}
watch(
() => query.value,
(newQuery) => {
const tabs = useTabStore();
if (tabs.current !== "search") {
tabs.switchToSearch();
}
switch (currentTab.value) {
case "tracks":
fetchTracks(newQuery);
break;
case "albums":
fetchAlbums(newQuery);
break;
case "artists":
fetchArtists(newQuery);
break;
default:
fetchTracks(newQuery);
break;
}
}
);
watch(
() => currentTab.value,
(newTab) => {
switch (newTab) {
case "tracks":
fetchTracks(query.value);
break;
case "albums":
fetchAlbums(query.value);
break;
case "artists":
fetchArtists(query.value);
break;
default:
fetchTracks(query.value);
break;
}
}
);
function changeTab(tab: string) {
currentTab.value = tab;
}
setTimeout(() => {}, 3000);
return {
tracks,
albums,
artists,
query,
currentTab,
loadTracks,
loadAlbums,
loadArtists,
changeTab,
};
});
+2 -2
View File
@@ -1,5 +1,5 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import perks from "../composables/perks"; import { focusCurrent } from "../composables/perks";
const tablist = { const tablist = {
home: "home", home: "home",
@@ -16,7 +16,7 @@ export default defineStore("tabs", {
changeTab(tab: string) { changeTab(tab: string) {
if (tab === this.tabs.queue) { if (tab === this.tabs.queue) {
setTimeout(() => { setTimeout(() => {
perks.focusCurrent(); focusCurrent();
}, 500); }, 500);
} }
this.current = tab; this.current = tab;
+70
View File
@@ -0,0 +1,70 @@
export default {
directives: {
"slide-from-left": {
initial: {
opacity: 0,
x: 0,
y: 20
},
enter: {
opacity: 1,
x: 0,
y: 0,
transition: {
duration: 100,
ease: "circInOut",
},
},
},
"slide-from-left-100": {
initial: {
opacity: 0,
x: -20,
},
enter: {
opacity: 1,
x: 0,
transition: {
delay: 100,
},
},
},
"slide-from-top": {
initial: {
y: -20,
opacity: 0,
},
enter: {
y: 0,
opacity: 1,
transition: {
delay: 200,
},
},
},
"slide-from-right": {
initial: {
x: 20,
opacity: 0,
},
enter: {
x: 0,
opacity: 1,
transition: {
delay: 200,
},
},
},
scale: {
initial: {
scale: 0.8,
},
enter: {
scale: 1,
transition: {
duration: 200,
},
},
},
},
};
+5 -13
View File
@@ -1,23 +1,15 @@
<template> <template>
<div class="home"> <div class="home">
<AlbumOfTheDay /> <home />
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import AlbumOfTheDay from "@/components/AlbumOfTheDay.vue"; import Home from "@/components/Home.vue";
export default {
name: "Home",
components: {
AlbumOfTheDay,
},
};
</script> </script>
<style> <style>
.home { .home {
padding-left: 20px; padding-left: 20px;
text-align: center; text-align: center;
} }
</style> </style>
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"jsx": "preserve",
"paths": {
"baseUrl": ["./"],
"@/*": ["./src/*"]
}
}
}
+94
View File
@@ -376,6 +376,40 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.29.tgz#07dac7051117236431d2f737d16932aa38bbb925" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.29.tgz#07dac7051117236431d2f737d16932aa38bbb925"
integrity sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw== integrity sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw==
"@vueuse/core@^8.1.2", "@vueuse/core@^8.5.0":
version "8.5.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-8.5.0.tgz#2b7548e52165c88e1463756c36188e105d806543"
integrity sha512-VEJ6sGNsPlUp0o9BGda2YISvDZbhWJSOJu5zlp2TufRGVrLcYUKr31jyFEOj6RXzG3k/H4aCYeZyjpItfU8glw==
dependencies:
"@vueuse/metadata" "8.5.0"
"@vueuse/shared" "8.5.0"
vue-demi "*"
"@vueuse/metadata@8.5.0":
version "8.5.0"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-8.5.0.tgz#1aaa3787922cfda0f38243aaa7779366a669b4db"
integrity sha512-WxsD+Cd+bn+HcjpY6Dl9FJ8ywTRTT9pTwk3bCQpzEhXVYAyNczKDSahk50fCfIJKeWHhyI4B2+/ZEOxQAkUr0g==
"@vueuse/motion@^2.0.0-beta.18":
version "2.0.0-beta.18"
resolved "https://registry.yarnpkg.com/@vueuse/motion/-/motion-2.0.0-beta.18.tgz#e98e9a4c34da2ca456a10639dca3b36409e97b5f"
integrity sha512-mPeXxuqZp13lqpcb+345TnEP7tEOjC/wTkwf8be1Obzt3913lPpZPXgwKafMoocKRNOnMZye8Y6PqQOEKztk9A==
dependencies:
"@vueuse/core" "^8.1.2"
"@vueuse/shared" "^8.1.2"
csstype "^3.0.11"
framesync "^6.1.0"
popmotion "^11.0.3"
style-value-types "^5.1.0"
vue-demi "*"
"@vueuse/shared@8.5.0", "@vueuse/shared@^8.1.2":
version "8.5.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-8.5.0.tgz#fa01ecd3161933f521dd6428b9ef8015ded1bbd3"
integrity sha512-qKG+SZb44VvGD4dU5cQ63z4JE2Yk39hQUecR0a9sEdJA01cx+XrxAvFKJfPooxwoiqalAVw/ktWK6xbyc/jS3g==
dependencies:
vue-demi "*"
"@webassemblyjs/ast@1.11.1": "@webassemblyjs/ast@1.11.1":
version "1.11.1" version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
@@ -717,6 +751,11 @@ csstype@^2.6.8:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa"
integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ== integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==
csstype@^3.0.11:
version "3.1.0"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
debug@^4.1.1, debug@^4.3.2: debug@^4.1.1, debug@^4.3.2:
version "4.3.3" version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
@@ -729,6 +768,11 @@ deep-is@^0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
defu@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/defu/-/defu-6.0.0.tgz#b397a6709a2f3202747a3d9daf9446e41ad0c5fc"
integrity sha512-t2MZGLf1V2rV4VBZbWIaXKdX/mUcYW0n2znQZoADBkGGxYL8EWqCuCZBmJPJ/Yy9fofJkyuuSuo5GSwo0XdEgw==
doctrine@^3.0.0: doctrine@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@@ -1071,6 +1115,20 @@ follow-redirects@^1.14.8:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
framesync@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.0.1.tgz#5e32fc01f1c42b39c654c35b16440e07a25d6f20"
integrity sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==
dependencies:
tslib "^2.1.0"
framesync@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.1.0.tgz#b22cf9afba52a9a895668b09e033b6a61e901c41"
integrity sha512-aBX+hdWAvwiJYeQlFLY2533VxeL6OEu71CAgV4GGKksrj6+dE6i7K86WSSiRBEARCoJn5bFqffhg4l07eA27tg==
dependencies:
tslib "^2.3.1"
fs.realpath@^1.0.0: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1162,6 +1220,11 @@ has@^1.0.3:
dependencies: dependencies:
function-bind "^1.1.1" function-bind "^1.1.1"
hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
ieee754@^1.1.13: ieee754@^1.1.13:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -1569,6 +1632,16 @@ pngjs@^3.0.0, pngjs@^3.3.3:
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
popmotion@^11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9"
integrity sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==
dependencies:
framesync "6.0.1"
hey-listen "^1.0.8"
style-value-types "5.0.0"
tslib "^2.1.0"
postcss@^8.1.10, postcss@^8.4.5: postcss@^8.1.10, postcss@^8.4.5:
version "8.4.6" version "8.4.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1"
@@ -1765,6 +1838,22 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-value-types@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad"
integrity sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==
dependencies:
hey-listen "^1.0.8"
tslib "^2.1.0"
style-value-types@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.1.0.tgz#228b02bd9418c57db46c1f450b85577e634a877f"
integrity sha512-DRIfBtjxQ4ztBZpexkFcI+UR7pODC5qLMf2Syt+bH98PAHHRH2tQnzxBuDQlqcAoYar6GzWnj8iAfqfwnEzCiQ==
dependencies:
hey-listen "^1.0.8"
tslib "^2.3.1"
supports-color@^7.1.0: supports-color@^7.1.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@@ -1831,6 +1920,11 @@ to-regex-range@^5.0.1:
dependencies: dependencies:
is-number "^7.0.0" is-number "^7.0.0"
tslib@^2.1.0, tslib@^2.3.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
type-check@^0.4.0, type-check@~0.4.0: type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"