↩️ 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"
},
"dependencies": {
"@vueuse/core": "^8.5.0",
"@vueuse/motion": "^2.0.0-beta.18",
"axios": "^0.26.1",
"defu": "^6.0.0",
"mitt": "^3.0.0",
"node-vibrant": "^3.2.1-alpha.1",
"pinia": "^2.0.11",
+88 -31
View File
@@ -1,11 +1,10 @@
"""
Contains all the search routes.
"""
from flask import Blueprint, request
from app.lib import searchlib
from app import helpers
from app.lib import searchlib
from flask import Blueprint
from flask import request
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")
def search():
"""
@@ -23,25 +79,14 @@ def search():
"""
query = request.args.get("q") or "Mexican girl"
albums = searchlib.get_search_albums(query)
artists_dicts = []
albums = searchlib.SearchAlbums(query)()
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:
for artist in song.artists:
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]
_tracks = searchlib.GetTopArtistTracks(top_artist)()
tracks = [*tracks, *[t for t in _tracks if t not in tracks]]
SEARCH_RESULTS.clear()
SEARCH_RESULTS["tracks"] = tracks
@@ -50,9 +95,18 @@ def search():
return {
"data": [
{"tracks": tracks[:5], "more": len(tracks) > 5},
{"albums": albums[:6], "more": len(albums) > 6},
{"artists": artists_dicts[:6], "more": len(artists_dicts) > 6},
{
"tracks": tracks[:5],
"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.
"""
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":
return {
"tracks": SEARCH_RESULTS["tracks"][start : start + 5],
"more": len(SEARCH_RESULTS["tracks"]) > start + 5,
"tracks": SEARCH_RESULTS["tracks"][index:index + 5],
"more": len(SEARCH_RESULTS["tracks"]) > index + 5,
}
elif type == "albums":
return {
"albums": SEARCH_RESULTS["albums"][start : start + 6],
"more": len(SEARCH_RESULTS["albums"]) > start + 6,
"albums": SEARCH_RESULTS["albums"][index:index + 6],
"more": len(SEARCH_RESULTS["albums"]) > index + 6,
}
elif type == "artists":
return {
"artists": SEARCH_RESULTS["artists"][start : start + 6],
"more": len(SEARCH_RESULTS["artists"]) > start + 6,
"artists": SEARCH_RESULTS["artists"][index:index + 6],
"more": len(SEARCH_RESULTS["artists"]) > index + 6,
}
+19 -10
View File
@@ -1,11 +1,11 @@
"""
This module contains mini functions for the server.
"""
from datetime import datetime
import os
import random
import threading
import time
from datetime import datetime
from typing import Dict
from typing import List
@@ -27,7 +27,9 @@ def 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.
"""
@@ -60,12 +62,10 @@ def remove_duplicates(tracklist: List[models.Track]) -> List[models.Track]:
while song_num < len(tracklist) - 1:
for index, song in enumerate(tracklist):
if (
tracklist[song_num].title == song.title
and tracklist[song_num].album == song.album
and tracklist[song_num].artists == song.artists
and index != song_num
):
if (tracklist[song_num].title == song.title
and tracklist[song_num].album == song.album
and tracklist[song_num].artists == song.artists
and index != song_num):
tracklist.remove(song)
song_num += 1
@@ -108,7 +108,8 @@ def check_artist_image(image: str) -> str:
"""
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()
else:
return img_name
@@ -120,7 +121,15 @@ def create_album_hash(title: str, artist: str) -> str:
"""
return (title + artist).replace(" ", "").lower()
def create_new_date():
now = datetime.now()
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 '/\\:*?"<>|'])
+11 -4
View File
@@ -6,13 +6,13 @@ import urllib
from typing import List
from app import api
from app import helpers
from app import instances
from app import models
from app.lib import taglib
from app.lib import trackslib
from progress.bar import Bar
from tqdm import tqdm
from app import helpers
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:
"""
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
@@ -62,8 +66,11 @@ def find_album(albums: List[models.Album], hash: str) -> int | None:
iter += 1
mid = (left + right) // 2
if albums[mid].hash == hash:
return mid
try:
if albums[mid].hash == hash:
return mid
except AttributeError:
print(albums)
if albums[mid].hash < hash:
left = mid + 1
@@ -151,7 +158,7 @@ def get_album_tracks(album: str, artist: str) -> List:
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.
"""
+38 -20
View File
@@ -1,25 +1,26 @@
import os
import time
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from multiprocessing import Pool
from os import path
import time
from typing import List
from tqdm import tqdm
from app import api
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 tracks_instance
from app.lib import folderslib
from app.lib.albumslib import create_album
from app.lib.albumslib import find_album
from app.lib.taglib import get_tags
from app.logger import Log
from app.models import Album, 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:
@@ -46,7 +47,7 @@ class Populate:
def run(self):
self.check_untagged()
self.tag_all_files()
self.get_all_tags()
if len(self.tagged_tracks) == 0:
return
@@ -76,6 +77,17 @@ class Populate:
Log(f"Found {len(self.files)} untagged tracks")
def process_tags(self, tags: dict):
for t in tags:
if t is None:
continue
t["albumhash"] = create_album_hash(t["album"], t["albumartist"])
self.tagged_tracks.append(t)
api.DB_TRACKS.append(t)
self.folders.add(t["folder"])
def get_tags(self, file: str):
tags = get_tags(file)
@@ -83,19 +95,27 @@ class Populate:
folder = tags["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)
api.DB_TRACKS.append(tags)
def tag_all_files(self):
def get_all_tags(self):
"""
Loops through all the untagged files and tags them.
"""
s = time.time()
print(f"Started tagging files")
with ThreadPoolExecutor() as executor:
executor.map(self.get_tags, self.files)
# print(f"Started tagging files")
# with ThreadPoolExecutor() as executor:
# 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
Log(f"Tagged {len(self.tagged_tracks)} files in {d} seconds")
@@ -149,9 +169,8 @@ class Populate:
for album in tqdm(self.pre_albums, desc="Building albums"):
self.create_album(album)
Log(
f"{self.exist_count} of {len(self.pre_albums)} albums were already in the database"
)
Log(f"{self.exist_count} of {len(self.pre_albums)} albums were already in the database"
)
def create_track(self, track: dict):
"""
@@ -186,9 +205,8 @@ class Populate:
with ThreadPoolExecutor() as executor:
executor.map(self.create_track, self.tagged_tracks)
Log(
f"Added {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums"
)
Log(f"Added {len(self.tagged_tracks)} new tracks and {len(self.albums)} new albums"
)
def save_albums(self):
"""
+148 -8
View File
@@ -1,19 +1,156 @@
"""
This library contains all the functions related to the search functionality.
"""
from typing import List
from app import models, helpers
from app.lib import albumslib
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:
"""
Gets all songs with a given title.
Holds all the default cutoff values.
"""
tracks = [track for track in api.TRACKS if query.lower() in track.title.lower()]
return helpers.remove_duplicates(tracks)
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.
"""
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]:
@@ -27,4 +164,7 @@ def get_artists(artist: str) -> List[models.Track]:
"""
Gets all songs with a given artist.
"""
return [track for track in api.TRACKS if artist.lower() in str(track.artists).lower()]
return [
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
def get_tags(fullpath: str) -> dict:
def get_tags(fullpath: str) -> dict | None:
"""
Returns a dictionary of tags for a given file.
"""
+18 -9
View File
@@ -7,14 +7,14 @@ import time
from app import api
from app import instances
from app import models
from app.helpers import create_album_hash
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 watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer
from app.helpers import create_album_hash
class OnMyWatch:
"""
@@ -50,18 +50,19 @@ def add_track(filepath: str) -> None:
tags = get_tags(filepath)
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)
albumindex = find_album(tags["album"], tags["albumartist"])
albumindex = find_album(api.ALBUMS, hash)
if albumindex is not None:
album = api.ALBUMS[albumindex]
else:
album_data = create_album(tags, api.DB_TRACKS)
instances.album_instance.insert_album(album_data)
album = models.Album(album_data)
instances.album_instance.insert_album(album)
api.ALBUMS.append(album)
tags["image"] = album.image
@@ -86,7 +87,8 @@ def remove_track(filepath: str) -> None:
fpath = filepath.replace(fname, "")
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:
print(f"💙 Watchdog Error: Error removing track {filepath} TypeError")
return
@@ -152,7 +154,14 @@ class Handler(PatternMatchingEventHandler):
Fired when a created file is closed.
"""
print("⚫ closed ~~~")
self.files_to_process.remove(event.src_path)
try:
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)
+25 -8
View File
@@ -6,8 +6,8 @@ from dataclasses import field
from typing import List
from app import api
from app.exceptions import TrackExistsInPlaylist
from app import helpers
from app.exceptions import TrackExistsInPlaylist
@dataclass(slots=True)
@@ -18,7 +18,7 @@ class Track:
trackid: str
title: str
artists: str
artists: list
albumartist: str
album: str
folder: str
@@ -32,8 +32,11 @@ class Track:
albumhash: str
def __init__(self, tags):
try:
self.trackid = tags["_id"]["$oid"]
except KeyError:
print(tags)
self.trackid = tags["_id"]["$oid"]
self.title = tags["title"]
self.artists = tags["artists"].split(", ")
self.albumartist = tags["albumartist"]
@@ -49,6 +52,22 @@ class Track:
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
class Album:
"""
@@ -78,11 +97,9 @@ class Album:
def get_p_track(ptrack):
for track in api.TRACKS:
if (
track.title == ptrack["title"]
and track.artists == ptrack["artists"]
and ptrack["album"] == track.album
):
if (track.title == ptrack["title"]
and track.artists == ptrack["artists"]
and ptrack["album"] == track.album):
return track
+153 -1
View File
@@ -118,6 +118,14 @@ category = "main"
optional = false
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]]
name = "jinja2"
version = "3.0.3"
@@ -181,6 +189,20 @@ snappy = ["python-snappy"]
srv = ["dnspython (>=1.16.0,<3.0.0)"]
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]]
name = "requests"
version = "2.27.1"
@@ -262,7 +284,7 @@ watchdog = ["watchdog"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "a2a0355e78fe2881e226dabda21b6d972a98aa4f1d60bf9f7f74957bb8ba6bea"
content-hash = "143a7a7f2158b3c3abfcfec207443a90f1a3d31a6935dfdf5e5e7452e226ca58"
[metadata.files]
certifi = [
@@ -309,6 +331,88 @@ itsdangerous = [
{file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"},
{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 = [
{file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
{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.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 = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
{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"
"colorgram.py" = "^1.2.0"
tqdm = "^4.64.0"
rapidfuzz = "^2.0.11"
[tool.poetry.dev-dependencies]
+15 -5
View File
@@ -5,8 +5,18 @@
#python manage.py
gpath=$(poetry run which gunicorn)
cd app
"$gpath" -b 0.0.0.0:9877 -w 4 --threads=2 "imgserver:app" &
echo "Booted image server"
cd ../
"$gpath" -b 0.0.0.0:9876 -w 1 --threads=4 "manage:create_app()" #--log-level=debug
while getopts ':s' opt; do
case $opt in
s)
echo "🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴🔴"
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 />
<Modal />
<Notification />
<div class="l-container" :class="{ collapsed: collapsed }">
<div class="l-sidebar">
<div id="logo-container">
<router-link :to="{ name: 'Home' }" v-if="!collapsed"
><div class="logo"></div
></router-link>
</div>
<div class="l-container">
<div class="l-sidebar rounded">
<Logo />
<Navigation />
<div class="l-album-art">
<nowPlaying />
@@ -24,7 +20,7 @@
</div>
</template>
<script setup>
<script setup lang="ts">
import Navigation from "./components/LeftSidebar/Navigation.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 useQStore from "./stores/queue";
import listenForKeyboardEvents from "./composables/keyboard";
import Logo from "./components/Logo.vue";
const RightSideBar = Main;
const context_store = useContextStore();
const queue = useQStore();
const app_dom = document.getElementById("app");
queue.readQueueFromLocalStorage();
queue.readQueue();
listenForKeyboardEvents(queue);
app_dom.addEventListener("click", (e) => {
@@ -55,38 +52,22 @@ app_dom.addEventListener("click", (e) => {
</script>
<style lang="scss">
@import "./assets/css/mixins.scss";
.l-sidebar {
position: relative;
.l-album-art {
width: calc(100% - 2rem);
position: absolute;
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 {
&::-webkit-scrollbar {
+2 -36
View File
@@ -8,45 +8,15 @@
height: 100%;
padding-right: $small;
@include phone-only {
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 {
width: 100%;
overflow: hidden;
display: flex;
display: grid;
grid-template-columns: 12rem 1fr 12rem;
align-items: center;
padding: $small;
@@ -55,10 +25,6 @@
align-items: center;
}
.controls-bottom {
width: min-content;
}
.progress-bottom {
width: 100%;
+7 -10
View File
@@ -1,4 +1,5 @@
@import "../css/ProgressBar.scss";
@import "mixins.scss";
:root {
--separator: #ffffff46;
@@ -20,6 +21,8 @@ body {
image-rendering: -webkit-optimize-contrast;
}
.heading {
font-size: 2rem;
font-weight: bold;
@@ -39,7 +42,6 @@ a {
}
.border {
// border: solid 1px $gray5;
background-color: $black;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
}
@@ -95,11 +97,11 @@ a {
}
.l-sidebar {
width: 15rem;
width: 17rem;
grid-area: l-sidebar;
padding-top: 0.5rem;
// border-right: solid 1px $gray3;
background-color: $black;
background-color: $gray;
margin: $small;
padding: 1rem;
}
.bottom-bar {
@@ -107,11 +109,6 @@ a {
height: 4rem;
}
.collapsed .l-sidebar {
width: 3rem;
transition: all 0.3s ease;
}
.ellip {
display: -webkit-box;
-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="art">
<div
class="image shadow-lg"
class="image shadow-lg rounded"
:style="{
backgroundImage: `url(&quot;${imguri + props.album.image}&quot;)`,
}"
v-motion-slide-from-left
></div>
</div>
<div class="info">
<div class="top">
<div class="top" v-motion-slide-from-top>
<div
class="h"
v-if="props.album.artist.toLowerCase() == 'various artists'"
>
Compilation
</div>
@@ -23,8 +25,9 @@
<div class="bottom">
<div class="stats">
{{ props.album.count }} Tracks
{{ perks.formatSeconds(props.album.duration, "long") }}
{{ props.album.date }} {{ props.album.artist }}
{{ formatSeconds(props.album.duration, true) }}
{{ props.album.date }}
{{ props.album.artist }}
</div>
<PlayBtnRect :source="playSources.album" />
</div>
@@ -34,11 +37,11 @@
</template>
<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 { formatSeconds } from "../../composables/perks";
import { paths } from "../../config";
import { AlbumInfo } from "../../interfaces";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
const imguri = paths.images.thumb;
const props = defineProps<{
@@ -89,7 +92,7 @@ extrackColors();
.title {
font-size: 2.5rem;
font-weight: 1000;
font-weight: 600;
color: white;
text-transform: capitalize;
}
+13 -17
View File
@@ -1,37 +1,33 @@
<!-- <template>
<template>
<div class="b-bar">
<div class="grid rounded">
<div class="controlsx rounded">
<div class="controls controls-bottom">
<div class="controls-bottom">
<HotKeys />
</div>
<div class="progress progress-bottom">
<span class="durationx">{{ formatSeconds(current_pos) }}</span>
<span class="durationx">{{ formatSeconds(q.track.current_time) }}</span>
<Progress />
<span class="durationx">{{
formatSeconds(state.current.value.length)
}}</span>
<span class="durationx">{{ formatSeconds(q.length) }}</span>
</div>
<div class="r-group">
<div id="heart" class="image ctrl-btn"></div>
<div id="add-to" class="image ctrl-btn"></div>
<div id="repeat" class="image ctrl-btn"></div>
</div>
<div class="controls controls-bottom"></div>
</div>
<div class="volume-group"></div>
</div>
</div>
</template>
<script setup>
import "../../assets/css/BottomBar/BottomBar.scss";
import Progress from "../shared/Progress.vue";
import HotKeys from "../shared/HotKeys.vue";
import state from "../../composables/state";
import perks from "../../composables/perks";
import playAudio from "../../composables/playAudio";
<script setup lang="ts">
import "@/assets/css/BottomBar/BottomBar.scss";
import Progress from "../LeftSidebar/NP/Progress.vue";
import HotKeys from "../LeftSidebar/NP/HotKeys.vue";
import { formatSeconds } from "@/composables/perks";
const current_pos = playAudio.current_time;
const formatSeconds = perks.formatSeconds;
</script> -->
import useQStore from "@/stores/queue";
const q = useQStore();
</script>
+5 -1
View File
@@ -84,7 +84,7 @@ function updateQueue(track: Track) {
.table {
width: 100%;
height: 100%;
overflow-y: auto;
overflow-y: hidden;
.current {
a {
@@ -141,6 +141,10 @@ function updateQueue(track: Track) {
}
.songlist {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
.context-on {
background-color: $gray4;
color: $white !important;
+11 -13
View File
@@ -1,33 +1,31 @@
<template>
<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
class="image ctrl-btn play-pause"
@click="playPause"
:class="{ isPlaying: props.playing }"
@click="q.playPause"
:class="{ isPlaying: q.playing }"
></div>
<div class="image ctrl-btn" id="next" @click="props.next"></div>
<div class="image ctrl-btn" id="next" @click="q.playNext"></div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
playing: boolean;
playPause: () => void;
next: () => void;
prev: () => void;
}>();
import useQStore from "@/stores/queue";
const q = useQStore();
</script>
<style lang="scss">
.hotkeys {
display: grid;
grid-template-columns: repeat(3, 1fr);
width: 100%;
gap: $small;
height: 3rem;
height: 2.5rem;
align-items: center;
justify-content: center;
place-content: flex-end;
width: 100%;
.ctrl-btn {
height: 2.5rem;
@@ -37,7 +35,7 @@ const props = defineProps<{
border-radius: 0.5rem;
&:hover {
background-color: $red;
background-color: $accent;
}
}
+9 -9
View File
@@ -2,7 +2,7 @@
<input
id="progress"
type="range"
:value="props.pos"
:value="q.track.current_time"
min="0"
max="100"
step="0.1"
@@ -11,13 +11,13 @@
</template>
<script setup lang="ts">
const seek = () => {
const value = Number(document.getElementById("progress").value);
props.seek(value);
};
import useQStore from "../../../stores/queue";
const props = defineProps<{
pos: number;
seek: (time: number) => void;
}>();
const q = useQStore();
const seek = () => {
const elem = <HTMLFormElement>document.getElementById("progress");
const value = elem.value;
q.seek(value);
};
</script>
+11 -31
View File
@@ -1,11 +1,16 @@
<template>
<div class="side-nav-container" :class="{ collapsed: props.collapsed }">
<div class="side-nav-container">
<router-link
v-for="menu in menus"
:key="menu.name"
: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="nav-icon image" :id="`${menu.name}-icon`"></div>
<span>{{ menu.name }}</span>
@@ -15,7 +20,7 @@
</div>
</template>
<script setup>
<script setup lang="ts">
const menus = [
{
name: "home",
@@ -46,36 +51,12 @@ const menus = [
route_name: "SettingsView",
},
];
const props = defineProps({
collapsed: {
type: Boolean,
default: false,
},
});
</script>
<style lang="scss">
.collapsed {
.nav-button {
margin-top: 5px;
span {
display: none;
}
.in {
width: 100%;
flex-direction: column;
}
}
}
.side-nav-container {
color: #fff;
margin-bottom: 1rem;
padding: 10px $small $small;
margin-top: 1rem;
margin: 1rem 0;
text-transform: capitalize;
.nav-button {
@@ -83,11 +64,10 @@ const props = defineProps({
display: flex;
align-items: flex-start;
justify-content: flex-start;
height: 100%;
padding: 0.6rem 0 0.6rem 0;
padding: 0.6rem 0;
&:hover {
background-color: $gray;
background-color: $gray3;
}
.nav-icon {
+2 -14
View File
@@ -1,9 +1,5 @@
<template>
<div
class="side-nav-container rounded hidden"
:class="{ hidden: collapsed }"
id="pinned-container"
>
<div class="side-nav-container rounded hidden" id="pinned-container">
<div>
<div class="nav-button" id="pinned-button">
<span id="text">Quick access</span>
@@ -53,13 +49,6 @@
</div>
</template>
<script>
export default {
props: ["collapsed"],
setup() {},
};
</script>
<style lang="scss">
#pinned-container {
background: #6e2c00;
@@ -102,5 +91,4 @@ export default {
#pinned-container #pinned-button:hover {
background-color: transparent;
}
</style>
</style>
+2 -4
View File
@@ -34,12 +34,10 @@
</template>
<script setup lang="ts">
import perks from "../../composables/perks";
import { putCommas } from "../../composables/perks";
import { Track } from "../../interfaces";
import { paths } from "../../config";
const imguri = paths.images.thumb
const putCommas = perks.putCommas;
const imguri = paths.images.thumb;
const props = defineProps<{
track: Track;
+4 -10
View File
@@ -5,18 +5,13 @@
<div class="separator no-border"></div>
<div>
<SongCard :track="queue.current" />
<Progress :seek="queue.seek" :pos="queue.current_time" />
<HotKeys
:playing="queue.playing"
:playPause="queue.playPause"
:next="queue.playNext"
:prev="queue.playPrev"
/>
<Progress />
<HotKeys />
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
import SongCard from "./SongCard.vue";
import HotKeys from "./NP/HotKeys.vue";
import Progress from "./NP/Progress.vue";
@@ -28,9 +23,8 @@ const queue = useQStore();
.l_ {
padding: 1rem;
background-color: $primary;
margin: $small;
text-align: center;
width: 14rem;
width: 100%;
display: grid;
position: relative;
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>
</div>
<div class="last-updated">
<div class="last-updated" v-motion-slide-from-right>
<span class="status"
>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 useContextStore from "../../stores/context";
import { paths } from "../../config";
import { onBeforeUnmount } from "vue";
const imguri = paths.images.playlist
const imguri = paths.images.playlist;
const context = useContextStore();
const modal = useModalStore();
+3 -3
View File
@@ -17,12 +17,12 @@
</div>
</template>
<script setup>
import Search from "./Search.vue";
<script setup lang="ts">
import Search from "./Search/Main.vue";
import UpNext from "./Queue.vue";
import Main from "./Home/Main.vue";
import useTabStore from "../../stores/tabs";
// import Search from "./Searchh.vue";
const DashBoard = Main;
const tabs = useTabStore();
</script>
+1 -1
View File
@@ -51,7 +51,7 @@
<script>
import playAudio from "@/composables/playAudio.js";
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 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>
<div class="albums-results border">
<div class="heading">Albums</div>
<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>
<LoadMore v-if="more" @loadMore="loadMore" />
<LoadMore v-if="search.albums.more" @loadMore="loadMore()" />
</div>
</template>
<script>
import AlbumCard from "@/components/shared/AlbumCard.vue";
<script setup lang="ts">
import AlbumCard from "../../shared/AlbumCard.vue";
import LoadMore from "./LoadMore.vue";
import useSearchStore from "../../../stores/search";
export default {
props: ["albums", "more"],
components: {
AlbumCard,
LoadMore,
},
setup(props, { emit }) {
let counter = 0;
const search = useSearchStore();
function loadMore() {
counter += 6;
emit("loadMore", counter);
}
let counter = 0;
return {
loadMore,
};
},
};
function loadMore() {
counter += 6;
search.loadAlbums(counter);
}
</script>
<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>
<div class="tracks-results border" v-if="tracks">
<div class="heading">Tracks</div>
<TransitionGroup class="items" name="list">
<div id="tracks-results" v-if="search.tracks.value">
<TransitionGroup name="list">
<TrackItem
v-for="track in tracks"
v-for="track in search.tracks.value"
:key="track.trackid"
:track="track"
:isPlaying="queue.playing"
@@ -12,43 +11,38 @@
@PlayThis="updateQueue"
/>
</TransitionGroup>
<LoadMore v-if="more" @loadMore="loadMore" />
<LoadMore v-if="search.tracks.more" @loadMore="loadMore" />
</div>
</template>
<script setup lang="ts">
import LoadMore from "./LoadMore.vue";
import TrackItem from "../shared/TrackItem.vue";
import useQStore from "../../stores/queue";
import { Track } from "../../interfaces";
import TrackItem from "../../shared/TrackItem.vue";
import useQStore from "../../../stores/queue";
import { Track } from "../../../interfaces";
import useSearchStore from "../../../stores/search";
let counter = 0;
const queue = useQStore();
const props = defineProps<{
tracks: Track[];
more: boolean;
query: string;
}>();
const emit = defineEmits(["loadMore"]);
const search = useSearchStore();
function loadMore() {
counter += 5;
emit("loadMore", counter);
search.loadTracks(counter);
}
function updateQueue(track: Track) {
console.log(props.query);
queue.playFromSearch(props.query, props.tracks);
queue.playFromSearch(search.query, search.tracks.value);
queue.play(track);
}
</script>
<style lang="scss">
.right-search .tracks-results {
.right-search #tracks-results {
border-radius: 0.5rem;
padding: $small;
height: 100% !important;
overflow: hidden;
.list-enter-active,
.list-leave-active {
+27 -34
View File
@@ -1,47 +1,36 @@
<template>
<div class="gsearch-input">
<Filters :filters="search.filters" @removeFilter="removeFilter" />
<div class="input-loader">
<div id="gsearch-input">
<div id="ginner" tabindex="0">
<div class="icon image"></div>
<input
id="search"
class="rounded"
v-model="search.query"
placeholder="Search your library"
type="text"
@keyup.backspace="removeLastFilter"
@focus="focusThis()"
@blur="unfocusThis()"
/>
</div>
</div>
</template>
<script setup>
import Filters from "../Search/Filters.vue";
import Loader from "../shared/Loader.vue";
import useSearchStore from "../../stores/gsearch";
<script setup lang="ts">
import useSearchStore from "../../stores/search";
const search = useSearchStore();
function removeFilter(filter) {
search.removeFilter(filter);
function focusThis() {
document.getElementById("ginner").classList.add("focused");
}
let counter = 0;
function removeLastFilter() {
if (search.query === "") {
counter++;
if (counter > 0) {
search.removeLastFilter();
}
} else {
counter = 0;
}
function unfocusThis() {
document.getElementById("ginner").classList.remove("focused");
}
</script>
<style lang="scss">
.gsearch-input {
#gsearch-input {
padding: $small;
display: flex;
@@ -49,15 +38,20 @@ function removeLastFilter() {
display: none;
}
.input-loader {
#ginner {
width: 100%;
border-radius: 0.4rem;
position: relative;
display: flex;
gap: $small;
background-color: $gray4;
height: 2.25rem;
._loader {
position: absolute;
top: -0.15rem;
right: 2rem;
.icon {
width: 2rem;
background-image: url("../../assets/icons/search.svg");
background-size: 1.5rem;
margin-left: $smaller;
}
input {
@@ -66,16 +60,15 @@ function removeLastFilter() {
width: 100%;
border: none;
line-height: 2.25rem;
background-color: $black;
color: inherit;
font-size: 1rem;
padding-left: 0.75rem;
background-color: transparent;
outline: 2px solid transparent;
&:focus {
outline: solid $accent;
}
}
}
.focused {
outline: solid $accent;
}
}
</style>
+2 -2
View File
@@ -12,7 +12,7 @@
<p class="title ellip">{{ next.title }}</p>
<hr />
<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
}}</span>
</p>
@@ -23,7 +23,7 @@
<script setup lang="ts">
import { Track } from "../../../interfaces";
import perks from "../../../composables/perks";
import {putCommas} from "../../../composables/perks";
import { paths } from "../../../config";
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 class="info">
<div class="title" v-if="$route.name == 'Playlists'">Playlists</div>
<div class="folder" v-else-if="$route.name == 'FolderView'">
<div
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="icon image"></div>
<div class="ellip">
{{ $route.params.path.split("/").splice(-1)[0] }}
<!-- {{ $route.params.path.split("/").splice(-1)[0] }} -->
{{ $route.params.path }}
</div>
</div>
</div>
@@ -28,7 +61,7 @@
</div>
</template>
<script setup>
<script setup lang="ts">
import NavButtons from "./NavButtons.vue";
import Loader from "../shared/Loader.vue";
import Search from "./Search.vue";
-4
View File
@@ -5,10 +5,6 @@
</div>
</template>
<script setup>
console.log();
</script>
<style lang="scss">
#back-forward {
display: grid;
+1 -1
View File
@@ -5,7 +5,7 @@
type="search"
name=""
id=""
placeholder="Search this playlist"
placeholder="Search here"
class="rounded"
/>
</form>
+1 -1
View File
@@ -17,7 +17,7 @@ const imguri = paths.images.artist;
defineProps<{
artist: any;
color: string;
color?: string;
}>();
</script>
+1 -1
View File
@@ -30,7 +30,7 @@ function play() {
queue.play(queue.tracks[0]);
break;
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]);
break;
case playSources.playlist:
+6 -8
View File
@@ -10,9 +10,7 @@
<div
class="album-art image rounded"
:style="{
backgroundImage: `url(&quot;${
imguri + props.song.image
}&quot;`,
backgroundImage: `url(&quot;${imguri + props.song.image}&quot;`,
}"
@click="emitUpdate(props.song)"
>
@@ -30,7 +28,7 @@
<div class="ellip" v-if="props.song.artists[0] !== ''">
<span
class="artist"
v-for="artist in perks.putCommas(props.song.artists)"
v-for="artist in putCommas(props.song.artists)"
:key="artist"
>{{ artist }}</span
>
@@ -54,13 +52,13 @@
</div>
</router-link>
<div class="song-duration">
{{ perks.formatSeconds(props.song.length) }}
{{ formatSeconds(props.song.length) }}
</div>
</div>
</template>
<script setup lang="ts">
import perks from "../../composables/perks.js";
import { putCommas, formatSeconds } from "../../composables/perks";
import useContextStore from "../../stores/context";
import useModalStore from "../../stores/modal";
import useQueueStore from "../../stores/queue";
@@ -68,13 +66,13 @@ import { ContextSrc } from "../../composables/enums";
import { ref } from "vue";
import trackContext from "../../contexts/track_context";
import { Track } from "../../interfaces.js";
import { Track } from "../../interfaces";
import { paths } from "../../config";
const contextStore = useContextStore();
const context_on = ref(false);
const imguri = paths.images.thumb
const imguri = paths.images.thumb;
const showContextMenu = (e: Event) => {
e.preventDefault();
+2 -7
View File
@@ -36,7 +36,7 @@
<script setup lang="ts">
import { ref } from "vue";
import perks from "../../composables/perks";
import { putCommas } from "../../composables/perks";
import trackContext from "../../contexts/track_context";
import { Track } from "../../interfaces";
import { ContextSrc } from "../../composables/enums";
@@ -46,9 +46,8 @@ import useModalStore from "../../stores/modal";
import useQueueStore from "../../stores/queue";
import { paths } from "../../config";
const contextStore = useContextStore();
const imguri = paths.images.thumb
const imguri = paths.images.thumb;
const props = defineProps<{
track: Track;
@@ -78,9 +77,6 @@ const emit = defineEmits<{
(e: "PlayThis", track: Track): void;
}>();
const current = ref(perks.current);
const putCommas = perks.putCommas;
const playThis = (track: Track) => {
emit("PlayThis", track);
};
@@ -97,7 +93,6 @@ const playThis = (track: Track) => {
}
.track-item {
width: 26.55rem;
display: flex;
align-items: center;
border-radius: 0.5rem;
+5 -1
View File
@@ -64,11 +64,15 @@ export default function (queue: any) {
if (!key_down_fired) {
if (!ctrlKey) return;
e.preventDefault();
focusSearchBox();
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) => {
const scope = perks.getElem("app", "id");
const contextMenu = perks.getElem("context-menu", "class");
// ? compute what is the mouse position relative to the container element (scope)
const scope = getElem("app", "id");
const contextMenu = getElem("context-menu", "class");
// ? compute what is the mouse position relative to the container element
// (scope)
let { left: scopeOffsetX, top: scopeOffsetY } = scope.getBoundingClientRect();
scopeOffsetX = scopeOffsetX < 0 ? 0 : scopeOffsetX;
@@ -1,4 +1,4 @@
const putCommas = (artists) => {
const putCommas = (artists: string[]) => {
let result = [];
artists.forEach((i, index, artists) => {
@@ -24,18 +24,18 @@ function focusCurrent() {
}
}
function getElem(identifier, type) {
function getElem(id: string, type: string) {
switch (type) {
case "class": {
return document.getElementsByClassName(identifier)[0];
return document.getElementsByClassName(id)[0];
}
case "id": {
return document.getElementById(identifier);
return document.getElementById(id);
}
}
}
function formatSeconds(seconds) {
function formatSeconds(seconds: number, long?: boolean) {
// check if there are arguments
const date = new Date(seconds * 1000);
@@ -48,7 +48,7 @@ function formatSeconds(seconds) {
let _mm = mm < 10 ? `0${mm}` : mm;
let _ss = ss < 10 ? `0${ss}` : ss;
if (arguments[1]) {
if (long == true) {
if (hh === 1) {
_hh = hh + " Hour";
} else {
@@ -75,10 +75,4 @@ function formatSeconds(seconds) {
}
}
export default {
putCommas,
focusCurrent,
formatSeconds,
getElem,
};
export { 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,
};
+37 -21
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) => {
let timeout
let timeout;
return (...args) => {
if (immediate && !timeout) fn(...args)
clearTimeout(timeout)
if (immediate && !timeout) fn(...args);
clearTimeout(timeout);
timeout = setTimeout(() => {
fn(...args)
}, delay)
}
}
fn(...args);
}, 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) => ({
get() {
track()
return state.value
track();
return state.value;
},
set: debounce(
value => {
state.value = value
trigger()
},
delay,
immediate
(value) => {
state.value = value;
trigger();
},
delay,
immediate
),
}))
}
}));
};
export default useDebouncedRef
export default useDebouncedRef;
+5 -5
View File
@@ -137,14 +137,14 @@ export default async (
add_to_playlist,
play_next,
add_to_q,
add_to_fav,
// add_to_fav,
separator,
go_to_folder,
go_to_artist,
go_to_alb_artist,
// go_to_artist,
// go_to_alb_artist,
go_to_album,
separator,
del_track,
// separator,
// del_track,
];
return options;
+13 -8
View File
@@ -1,13 +1,18 @@
import { createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
import { createPinia } from 'pinia'
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);
app.use(createPinia())
app.use(createPinia());
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() {
const diff = new Date().getTime() - this.duration;
console.log(diff);
if (diff <= 250) {
setTimeout(() => {
+29 -17
View File
@@ -12,7 +12,7 @@ import {
import notif from "../composables/mediaNotification";
import { FromOptions } from "../composables/enums";
function addQToLocalStorage(
function writeQueue(
from: fromFolder | fromAlbum | fromPlaylist,
tracks: Track[]
) {
@@ -25,11 +25,11 @@ function addQToLocalStorage(
);
}
function addCurrentToLocalStorage(track: Track) {
function writeCurrent(track: Track) {
localStorage.setItem("current", JSON.stringify(track));
}
function readCurrentFromLocalStorage(): Track {
function readCurrent(): Track {
const current = localStorage.getItem("current");
if (current) {
return JSON.parse(current);
@@ -48,11 +48,14 @@ export default defineStore("Queue", {
state: () => ({
progressElem: HTMLElement,
audio: new Audio(),
track: {
current_time: 0,
duration: 0,
},
current: <Track>{},
next: <Track>{},
prev: <Track>{},
playing: false,
current_time: 0,
from: <fromFolder>{} || <fromAlbum>{} || <fromPlaylist>{},
tracks: <Track[]>[defaultTrack],
}),
@@ -68,14 +71,16 @@ export default defineStore("Queue", {
this.audio.onerror = reject;
})
.then(() => {
this.track.duration = this.audio.duration;
this.audio.play().then(() => {
this.playing = true;
notif(track, this.playPause, this.playNext, this.playPrev);
this.audio.ontimeupdate = () => {
this.current_time =
this.track.current_time =
(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 = () => {
@@ -114,7 +119,7 @@ export default defineStore("Queue", {
}
}
},
readQueueFromLocalStorage() {
readQueue() {
const queue = localStorage.getItem("queue");
if (queue) {
@@ -123,7 +128,7 @@ export default defineStore("Queue", {
this.tracks = parsed.tracks;
}
this.updateCurrent(readCurrentFromLocalStorage());
this.updateCurrent(readCurrent());
},
updateCurrent(track: Track) {
this.current = track;
@@ -131,7 +136,7 @@ export default defineStore("Queue", {
this.updateNext(this.current);
this.updatePrev(this.current);
addCurrentToLocalStorage(track);
writeCurrent(track);
},
updateNext(track: Track) {
const index = this.tracks.findIndex(
@@ -161,8 +166,9 @@ export default defineStore("Queue", {
},
setNewQueue(tracklist: Track[]) {
if (this.tracks !== tracklist) {
this.tracks = tracklist;
addQToLocalStorage(this.from, this.tracks);
this.tracks = [];
this.tracks.push(...tracklist);
writeQueue(this.from, this.tracks);
}
},
playFromFolder(fpath: string, tracks: Track[]) {
@@ -201,7 +207,8 @@ export default defineStore("Queue", {
},
addTrackToQueue(track: Track) {
this.tracks.push(track);
addQToLocalStorage(this.from, this.tracks);
writeQueue(this.from, this.tracks);
this.updateNext(this.current);
},
playTrackNext(track: Track) {
const Toast = useNotifStore();
@@ -209,19 +216,24 @@ export default defineStore("Queue", {
(t: Track) => t.trackid === this.current.trackid
);
const next: Track = this.tracks[currentid + 1];
if (currentid == this.tracks.length - 1) {
this.tracks.push(track);
} else {
const next: Track = this.tracks[currentid + 1];
if (next.trackid === track.trackid) {
Toast.showNotification("Track is already queued", NotifType.Info);
return;
if (next.trackid === track.trackid) {
Toast.showNotification("Track is already queued", NotifType.Info);
return;
}
}
this.tracks.splice(currentid + 1, 0, track);
this.updateNext(this.current);
Toast.showNotification(
`Added ${track.title} to queue`,
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 perks from "../composables/perks";
import { focusCurrent } from "../composables/perks";
const tablist = {
home: "home",
@@ -16,7 +16,7 @@ export default defineStore("tabs", {
changeTab(tab: string) {
if (tab === this.tabs.queue) {
setTimeout(() => {
perks.focusCurrent();
focusCurrent();
}, 500);
}
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,
},
},
},
},
};
+8 -16
View File
@@ -1,23 +1,15 @@
<template>
<div class="home">
<AlbumOfTheDay />
<home />
</div>
</template>
<script>
import AlbumOfTheDay from "@/components/AlbumOfTheDay.vue";
export default {
name: "Home",
components: {
AlbumOfTheDay,
},
};
<script setup lang="ts">
import Home from "@/components/Home.vue";
</script>
<style>
.home {
padding-left: 20px;
text-align: center;
}
</style>
.home {
padding-left: 20px;
text-align: center;
}
</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"
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":
version "1.11.1"
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"
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:
version "4.3.3"
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"
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:
version "3.0.0"
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"
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:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1162,6 +1220,11 @@ has@^1.0.3:
dependencies:
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:
version "1.2.1"
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"
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:
version "8.4.6"
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"
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:
version "7.2.0"
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:
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:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"