mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
move server code to alice-core repo
~ remove server code from this repo
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
from flask import Flask
|
||||
from flask_caching import Cache
|
||||
from flask_cors import CORS
|
||||
|
||||
config = {"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 300}
|
||||
|
||||
cache = Cache(config=config)
|
||||
|
||||
|
||||
def create_app():
|
||||
"""
|
||||
Creates the Flask instance, registers modules and registers all the API blueprints.
|
||||
"""
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
app.config.from_mapping(config)
|
||||
cache.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
from app.api import album, artist, folder, playlist, search, track
|
||||
|
||||
app.register_blueprint(album.album_bp, url_prefix="/")
|
||||
app.register_blueprint(artist.artist_bp, url_prefix="/")
|
||||
app.register_blueprint(track.track_bp, url_prefix="/")
|
||||
app.register_blueprint(search.search_bp, url_prefix="/")
|
||||
app.register_blueprint(folder.folder_bp, url_prefix="/")
|
||||
app.register_blueprint(playlist.playlist_bp, url_prefix="/")
|
||||
|
||||
return app
|
||||
@@ -1,21 +0,0 @@
|
||||
"""
|
||||
This module contains all the Flask Blueprints and API routes. It also contains all the globals list
|
||||
that are used through-out the app. It handles the initialization of the watchdog,
|
||||
checking and creating config dirs and starting the re-indexing process using a background thread.
|
||||
"""
|
||||
from app import functions
|
||||
from app import helpers
|
||||
from app import prep
|
||||
|
||||
|
||||
@helpers.background
|
||||
def initialize() -> None:
|
||||
"""
|
||||
Runs all the necessary setup functions.
|
||||
"""
|
||||
functions.start_watchdog()
|
||||
prep.create_config_dir()
|
||||
functions.run_checks()
|
||||
|
||||
|
||||
initialize()
|
||||
@@ -1,113 +0,0 @@
|
||||
"""
|
||||
Contains all the album routes.
|
||||
"""
|
||||
from pprint import pprint
|
||||
from typing import List
|
||||
|
||||
from app import api
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.functions import FetchAlbumBio
|
||||
from app.lib import albumslib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
album_bp = Blueprint("album", __name__, url_prefix="")
|
||||
|
||||
|
||||
@album_bp.route("/")
|
||||
def say_hi():
|
||||
"""Returns some text for the default route"""
|
||||
return "^ _ ^"
|
||||
|
||||
|
||||
@album_bp.route("/albums")
|
||||
def get_albums():
|
||||
"""returns all the albums"""
|
||||
albums = []
|
||||
|
||||
for song in api.DB_TRACKS:
|
||||
al_obj = {"name": song["album"], "artist": song["artists"]}
|
||||
|
||||
if al_obj not in albums:
|
||||
albums.append(al_obj)
|
||||
|
||||
return {"albums": albums}
|
||||
|
||||
|
||||
@album_bp.route("/album", methods=["POST"])
|
||||
def get_album():
|
||||
"""Returns all the tracks in the given album."""
|
||||
data = request.get_json()
|
||||
albumhash = data["hash"]
|
||||
error_msg = {"error": "Album not created yet."}
|
||||
|
||||
tracks = instances.tracks_instance.find_tracks_by_hash(albumhash)
|
||||
|
||||
if len(tracks) == 0:
|
||||
return error_msg, 204
|
||||
|
||||
tracks = [models.Track(t) for t in tracks]
|
||||
tracks = helpers.RemoveDuplicates(tracks)()
|
||||
|
||||
album = instances.album_instance.find_album_by_hash(albumhash)
|
||||
|
||||
if not album:
|
||||
return error_msg, 204
|
||||
|
||||
album = models.Album(album)
|
||||
|
||||
album.count = len(tracks)
|
||||
try:
|
||||
album.duration = sum([t.length for t in tracks])
|
||||
except AttributeError:
|
||||
album.duration = 0
|
||||
|
||||
if (
|
||||
album.count == 1
|
||||
and tracks[0].title == album.title
|
||||
and tracks[0].tracknumber == 1
|
||||
and tracks[0].discnumber == 1
|
||||
):
|
||||
album.is_single = True
|
||||
|
||||
return {"tracks": tracks, "info": album}
|
||||
|
||||
|
||||
@album_bp.route("/album/bio", methods=["POST"])
|
||||
def get_album_bio():
|
||||
"""Returns the album bio for the given album."""
|
||||
data = request.get_json()
|
||||
album_hash = data["hash"]
|
||||
err_msg = {"bio": "No bio found"}
|
||||
|
||||
album = instances.album_instance.find_album_by_hash(album_hash)
|
||||
|
||||
if album is None:
|
||||
return err_msg, 404
|
||||
|
||||
bio = FetchAlbumBio(album["title"], album["artist"])()
|
||||
|
||||
if bio is None:
|
||||
return err_msg, 404
|
||||
|
||||
return {"bio": bio}
|
||||
|
||||
|
||||
@album_bp.route("/album/artists", methods=["POST"])
|
||||
def get_albumartists():
|
||||
"""
|
||||
Returns a list of artists featured in a given album.
|
||||
"""
|
||||
|
||||
data = request.get_json()
|
||||
albumhash = data["hash"]
|
||||
|
||||
tracks = instances.tracks_instance.find_tracks_by_hash(albumhash)
|
||||
tracks = [models.Track(t) for t in tracks]
|
||||
|
||||
artists = [a for t in tracks for a in t.artists]
|
||||
artists = helpers.get_normalized_artists(artists)
|
||||
|
||||
return {"artists": artists}
|
||||
@@ -1,57 +0,0 @@
|
||||
"""
|
||||
Contains all the artist(s) routes.
|
||||
"""
|
||||
import urllib
|
||||
|
||||
from app import cache
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from flask import Blueprint
|
||||
|
||||
artist_bp = Blueprint("artist", __name__, url_prefix="/")
|
||||
|
||||
# @artist_bp.route("/artist/<artist>")
|
||||
# @cache.cached()
|
||||
# def get_artist_data(artist: str):
|
||||
# """Returns the artist's data, tracks and albums"""
|
||||
# artist = urllib.parse.unquote(artist)
|
||||
# artist_obj = instances.artist_instance.get_artists_by_name(artist)
|
||||
|
||||
# def get_artist_tracks():
|
||||
# songs = instances.tracks_instance.find_songs_by_artist(artist)
|
||||
|
||||
# return songs
|
||||
|
||||
# artist_songs = get_artist_tracks()
|
||||
# songs = helpers.remove_duplicates(artist_songs)
|
||||
|
||||
# def get_artist_albums():
|
||||
# artist_albums = []
|
||||
# albums_with_count = []
|
||||
|
||||
# albums = instances.tracks_instance.find_songs_by_albumartist(artist)
|
||||
|
||||
# for song in albums:
|
||||
# if song["album"] not in artist_albums:
|
||||
# artist_albums.append(song["album"])
|
||||
|
||||
# for album in artist_albums:
|
||||
# count = 0
|
||||
# length = 0
|
||||
|
||||
# for song in artist_songs:
|
||||
# if song["album"] == album:
|
||||
# count = count + 1
|
||||
# length = length + song["length"]
|
||||
|
||||
# album_ = {"title": album, "count": count, "length": length}
|
||||
|
||||
# albums_with_count.append(album_)
|
||||
|
||||
# return albums_with_count
|
||||
|
||||
# return {
|
||||
# "artist": artist_obj,
|
||||
# "songs": songs,
|
||||
# "albums": get_artist_albums()
|
||||
# }
|
||||
@@ -1,28 +0,0 @@
|
||||
"""
|
||||
Contains all the folder routes.
|
||||
"""
|
||||
from app import settings
|
||||
from app.lib.folderslib import getFnF
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
folder_bp = Blueprint("folder", __name__, url_prefix="/")
|
||||
|
||||
|
||||
@folder_bp.route("/folder", methods=["POST"])
|
||||
def get_folder_tree():
|
||||
"""
|
||||
Returns a list of all the folders and tracks in the given folder.
|
||||
"""
|
||||
data = request.get_json()
|
||||
req_dir: str = data["folder"]
|
||||
|
||||
if req_dir == "$home":
|
||||
req_dir = settings.HOME_DIR
|
||||
|
||||
tracks, folders = getFnF(req_dir)()
|
||||
|
||||
return {
|
||||
"tracks": tracks,
|
||||
"folders": sorted(folders, key=lambda i: i.name),
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
"""
|
||||
Contains all the playlist routes.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from app import exceptions
|
||||
from app import instances
|
||||
from app import models
|
||||
from app import serializer
|
||||
from app.helpers import create_new_date
|
||||
from app.helpers import Get
|
||||
from app.helpers import UseBisection
|
||||
from app.lib import playlistlib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
playlist_bp = Blueprint("playlist", __name__, url_prefix="/")
|
||||
|
||||
PlaylistExists = exceptions.PlaylistExistsError
|
||||
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylistError
|
||||
|
||||
|
||||
@playlist_bp.route("/playlists", methods=["GET"])
|
||||
def get_all_playlists():
|
||||
"""Returns all the playlists."""
|
||||
dbplaylists = instances.playlist_instance.get_all_playlists()
|
||||
dbplaylists = [models.Playlist(p) for p in dbplaylists]
|
||||
|
||||
playlists = [
|
||||
serializer.Playlist(p, construct_last_updated=False) for p in dbplaylists
|
||||
]
|
||||
playlists.sort(
|
||||
key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"),
|
||||
reverse=True,
|
||||
)
|
||||
return {"data": playlists}
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/new", methods=["POST"])
|
||||
def create_playlist():
|
||||
data = request.get_json()
|
||||
|
||||
data = {
|
||||
"name": data["name"],
|
||||
"description": "",
|
||||
"pre_tracks": [],
|
||||
"lastUpdated": create_new_date(),
|
||||
"image": "",
|
||||
"thumb": "",
|
||||
}
|
||||
|
||||
dbp = instances.playlist_instance.get_playlist_by_name(data["name"])
|
||||
|
||||
if dbp is not None:
|
||||
return {"message": "Playlist already exists."}, 409
|
||||
|
||||
upsert_id = instances.playlist_instance.insert_playlist(data)
|
||||
p = instances.playlist_instance.get_playlist_by_id(upsert_id)
|
||||
playlist = models.Playlist(p)
|
||||
|
||||
return {"playlist": playlist}, 201
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/<playlist_id>/add", methods=["POST"])
|
||||
def add_track_to_playlist(playlist_id: str):
|
||||
data = request.get_json()
|
||||
|
||||
trackid = data["track"]
|
||||
|
||||
try:
|
||||
playlistlib.add_track(playlist_id, trackid)
|
||||
except TrackExistsInPlaylist:
|
||||
return {"error": "Track already exists in playlist"}, 409
|
||||
|
||||
return {"msg": "I think It's done"}, 200
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/<playlistid>")
|
||||
def get_playlist(playlistid: str):
|
||||
p = instances.playlist_instance.get_playlist_by_id(playlistid)
|
||||
if p is None:
|
||||
return {"info": {}, "tracks": []}
|
||||
|
||||
playlist = models.Playlist(p)
|
||||
|
||||
tracks = playlistlib.create_playlist_tracks(playlist.pretracks)
|
||||
|
||||
duration = sum([t.length for t in tracks])
|
||||
playlist = serializer.Playlist(playlist)
|
||||
playlist.duration = duration
|
||||
|
||||
return {"info": playlist, "tracks": tracks}
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/<playlistid>/update", methods=["PUT"])
|
||||
def update_playlist(playlistid: str):
|
||||
image = None
|
||||
|
||||
if "image" in request.files:
|
||||
image = request.files["image"]
|
||||
|
||||
data = request.form
|
||||
|
||||
playlist = {
|
||||
"name": str(data.get("name")).strip(),
|
||||
"description": str(data.get("description").strip()),
|
||||
"lastUpdated": create_new_date(),
|
||||
"image": None,
|
||||
"thumb": None,
|
||||
}
|
||||
|
||||
playlists = Get.get_all_playlists()
|
||||
|
||||
p = UseBisection(playlists, "playlistid", [playlistid])()
|
||||
p: models.Playlist = p[0]
|
||||
|
||||
if playlist is not None:
|
||||
if image:
|
||||
image_, thumb_ = playlistlib.save_p_image(image, playlistid)
|
||||
playlist["image"] = image_
|
||||
playlist["thumb"] = thumb_
|
||||
else:
|
||||
playlist["image"] = p.image.split("/")[-1]
|
||||
playlist["thumb"] = p.thumb.split("/")[-1]
|
||||
|
||||
p.update_playlist(playlist)
|
||||
instances.playlist_instance.update_playlist(playlistid, playlist)
|
||||
|
||||
return {
|
||||
"data": serializer.Playlist(p),
|
||||
}
|
||||
|
||||
return {"msg": "Something shady happened"}, 500
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/artists", methods=["POST"])
|
||||
def get_playlist_artists():
|
||||
data = request.get_json()
|
||||
|
||||
pid = data["pid"]
|
||||
artists = playlistlib.GetPlaylistArtists(pid)()
|
||||
|
||||
return {"data": artists}
|
||||
@@ -1,216 +0,0 @@
|
||||
"""
|
||||
Contains all the search routes.
|
||||
"""
|
||||
from pprint import pprint
|
||||
from typing import List
|
||||
|
||||
from app import helpers
|
||||
from app import models
|
||||
from app import serializer
|
||||
from app.lib import searchlib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
search_bp = Blueprint("search", __name__, url_prefix="/")
|
||||
|
||||
SEARCH_RESULTS = {
|
||||
"tracks": [],
|
||||
"albums": [],
|
||||
"artists": [],
|
||||
}
|
||||
|
||||
|
||||
class SearchResults:
|
||||
"""
|
||||
Holds all the search results.
|
||||
"""
|
||||
|
||||
query: str = ""
|
||||
tracks: list[models.Track] = []
|
||||
albums: list[models.Album] = []
|
||||
playlists: list[models.Playlist] = []
|
||||
artists: list[models.Artist] = []
|
||||
|
||||
|
||||
class DoSearch:
|
||||
"""Class containing the methods that perform searching."""
|
||||
|
||||
def __init__(self, query: str) -> None:
|
||||
"""
|
||||
:param :str:`query`: the search query.
|
||||
"""
|
||||
self.query = query
|
||||
SearchResults.query = query
|
||||
|
||||
def search_tracks(self):
|
||||
"""Calls :class:`SearchTracks` which returns the tracks that fuzzily match
|
||||
the search terms. Then adds them to the `SearchResults` store.
|
||||
"""
|
||||
self.tracks = helpers.Get.get_all_tracks()
|
||||
tracks = searchlib.SearchTracks(self.tracks, self.query)()
|
||||
tracks = helpers.RemoveDuplicates(tracks)()
|
||||
SearchResults.tracks = tracks
|
||||
|
||||
return tracks
|
||||
|
||||
def search_artists(self):
|
||||
"""Calls :class:`SearchArtists` which returns the artists that fuzzily match
|
||||
the search term. Then adds them to the `SearchResults` store.
|
||||
"""
|
||||
self.artists = helpers.Get.get_all_artists()
|
||||
artists = searchlib.SearchArtists(self.artists, self.query)()
|
||||
SearchResults.artists = artists
|
||||
|
||||
return artists
|
||||
|
||||
def search_albums(self):
|
||||
"""Calls :class:`SearchAlbums` which returns the albums that fuzzily match
|
||||
the search term. Then adds them to the `SearchResults` store.
|
||||
"""
|
||||
albums = helpers.Get.get_all_albums()
|
||||
albums = searchlib.SearchAlbums(albums, self.query)()
|
||||
SearchResults.albums = albums
|
||||
|
||||
return albums
|
||||
|
||||
def search_playlists(self):
|
||||
"""Calls :class:`SearchPlaylists` which returns the playlists that fuzzily match
|
||||
the search term. Then adds them to the `SearchResults` store.
|
||||
"""
|
||||
playlists = helpers.Get.get_all_playlists()
|
||||
playlists = [serializer.Playlist(playlist) for playlist in playlists]
|
||||
|
||||
playlists = searchlib.SearchPlaylists(playlists, self.query)()
|
||||
SearchResults.playlists = playlists
|
||||
|
||||
return playlists
|
||||
|
||||
def search_all(self):
|
||||
"""Calls all the search methods."""
|
||||
self.search_tracks()
|
||||
self.search_albums()
|
||||
self.search_artists()
|
||||
self.search_playlists()
|
||||
|
||||
|
||||
@search_bp.route("/search/tracks", methods=["GET"])
|
||||
def search_tracks():
|
||||
"""
|
||||
Searches for tracks that match the search query.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
tracks = DoSearch(query).search_tracks()
|
||||
|
||||
return {
|
||||
"tracks": tracks[:6],
|
||||
"more": len(tracks) > 6,
|
||||
}, 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
|
||||
|
||||
tracks = DoSearch(query).search_albums()
|
||||
|
||||
return {
|
||||
"albums": tracks[:6],
|
||||
"more": len(tracks) > 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
|
||||
|
||||
artists = DoSearch(query).search_artists()
|
||||
|
||||
return {
|
||||
"artists": artists[:6],
|
||||
"more": len(artists) > 6,
|
||||
}, 200
|
||||
|
||||
|
||||
@search_bp.route("/search/playlists", methods=["GET"])
|
||||
def search_playlists():
|
||||
"""
|
||||
Searches for playlists.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
playlists = DoSearch(query).search_playlists()
|
||||
|
||||
return {
|
||||
"playlists": playlists[:6],
|
||||
"more": len(playlists) > 6,
|
||||
}, 200
|
||||
|
||||
|
||||
@search_bp.route("/search/top", methods=["GET"])
|
||||
def get_top_results():
|
||||
"""
|
||||
Returns the top results for the search query.
|
||||
"""
|
||||
|
||||
query = request.args.get("q")
|
||||
if not query:
|
||||
return {"error": "No query provided"}, 400
|
||||
|
||||
DoSearch(query).search_all()
|
||||
|
||||
max = 2
|
||||
return {
|
||||
"tracks": SearchResults.tracks[:max],
|
||||
"albums": SearchResults.albums[:max],
|
||||
"artists": SearchResults.artists[:max],
|
||||
"playlists": SearchResults.playlists[:max],
|
||||
}
|
||||
|
||||
|
||||
@search_bp.route("/search/loadmore")
|
||||
def search_load_more():
|
||||
"""
|
||||
Returns more songs, albums or artists from a search query.
|
||||
"""
|
||||
type = request.args.get("type")
|
||||
index = int(request.args.get("index"))
|
||||
|
||||
if type == "tracks":
|
||||
t = SearchResults.tracks
|
||||
return {
|
||||
"tracks": t[index:index + 5],
|
||||
"more": len(t) > index + 5,
|
||||
}
|
||||
|
||||
elif type == "albums":
|
||||
a = SearchResults.albums
|
||||
return {
|
||||
"albums": a[index:index + 6],
|
||||
"more": len(a) > index + 6,
|
||||
}
|
||||
|
||||
elif type == "artists":
|
||||
a = SearchResults.artists
|
||||
return {
|
||||
"artists": a[index:index + 6],
|
||||
"more": len(a) > index + 6,
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
"""
|
||||
Contains all the track routes.
|
||||
"""
|
||||
from app import api
|
||||
from app import instances
|
||||
from app import models
|
||||
from flask import Blueprint
|
||||
from flask import send_file
|
||||
|
||||
track_bp = Blueprint("track", __name__, url_prefix="/")
|
||||
|
||||
|
||||
@track_bp.route("/file/<trackid>")
|
||||
def send_track_file(trackid):
|
||||
"""
|
||||
Returns an audio file that matches the passed id to the client.
|
||||
"""
|
||||
track = instances.tracks_instance.get_track_by_id(trackid)
|
||||
msg = {"msg": "File Not Found"}
|
||||
|
||||
if track is None:
|
||||
return msg, 404
|
||||
|
||||
track = models.Track(track)
|
||||
type = track.filepath.split(".")[-1]
|
||||
|
||||
try:
|
||||
return send_file(track.filepath, mimetype=f"audio/{type}")
|
||||
except FileNotFoundError:
|
||||
return msg, 404
|
||||
|
||||
|
||||
@track_bp.route("/sample")
|
||||
def get_sample_track():
|
||||
"""
|
||||
Returns a sample track object.
|
||||
"""
|
||||
|
||||
return instances.tracks_instance.get_song_by_album("Legends Never Die",
|
||||
"Juice WRLD")
|
||||
@@ -1,214 +0,0 @@
|
||||
class AlbumMethods:
|
||||
"""
|
||||
Lists all the methods that can be found in the Albums class.
|
||||
"""
|
||||
|
||||
def insert_album():
|
||||
"""
|
||||
Inserts a new album object into the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_all_albums():
|
||||
"""
|
||||
Returns all the albums in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_album_by_id():
|
||||
"""
|
||||
Returns a single album matching the passed id.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_album_by_name():
|
||||
"""
|
||||
Returns a single album matching the passed name.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_album_by_artist():
|
||||
"""
|
||||
Returns a single album matching the passed artist name.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ArtistMethods:
|
||||
"""
|
||||
Lists all the methods that can be found in the Artists class.
|
||||
"""
|
||||
|
||||
def insert_artist():
|
||||
"""
|
||||
Inserts a new artist object into the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_all_artists():
|
||||
"""
|
||||
Returns all the artists in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_artist_by_id():
|
||||
"""
|
||||
Returns an artist matching the mongo Id.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_artists_by_name():
|
||||
"""
|
||||
Returns all the artists matching the query.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PlaylistMethods:
|
||||
"""
|
||||
Lists all the methods that can be found in the Playlists class.
|
||||
"""
|
||||
|
||||
def insert_playlist():
|
||||
"""
|
||||
Inserts a new playlist object into the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_all_playlists():
|
||||
"""
|
||||
Returns all the playlists in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_playlist_by_id():
|
||||
"""
|
||||
Returns a single playlist matching the id in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_track_to_playlist():
|
||||
"""
|
||||
Adds a track to a playlist.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_playlist_by_name():
|
||||
"""
|
||||
Returns a single playlist matching the name in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def update_playlist():
|
||||
"""
|
||||
Updates a playlist.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TrackMethods:
|
||||
"""
|
||||
Lists all the methods that can be found in the Tracks class.
|
||||
"""
|
||||
|
||||
def insert_track():
|
||||
"""
|
||||
Inserts a new track object into the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def drop_db():
|
||||
"""
|
||||
Drops the entire database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_all_tracks():
|
||||
"""
|
||||
Returns all the tracks in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_track_by_id():
|
||||
"""
|
||||
Returns a single track matching the id in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_track_by_album():
|
||||
"""
|
||||
Returns a single track matching the album in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def search_tracks_by_album():
|
||||
"""
|
||||
Returns all the tracks matching the albums in the query params (using regex).
|
||||
"""
|
||||
pass
|
||||
|
||||
def search_tracks_by_artist():
|
||||
"""
|
||||
Returns all the tracks matching the artists in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_track_by_title():
|
||||
"""
|
||||
Finds all the tracks matching the title in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_album():
|
||||
"""
|
||||
Finds all the tracks matching the album in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_folder():
|
||||
"""
|
||||
Finds all the tracks matching the folder in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_artist():
|
||||
"""
|
||||
Finds all the tracks matching the artist in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_albumartist():
|
||||
"""
|
||||
Finds all the tracks matching the album artist in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_track_by_path():
|
||||
"""
|
||||
Returns a single track matching the path in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_track_by_path():
|
||||
"""
|
||||
Removes a track from the database. Returns a boolean indicating success or failure of the operation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_track_by_id():
|
||||
"""
|
||||
Removes a track from the database. Returns a boolean indicating success or failure of the operation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_hash():
|
||||
"""
|
||||
Returns all the tracks matching the passed hash.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_dir_t_count():
|
||||
"""
|
||||
Returns a list of all the tracks matching the path in the query params.
|
||||
"""
|
||||
pass
|
||||
@@ -1,80 +0,0 @@
|
||||
"""
|
||||
This module creates and initiliazes a MongoDB instance. It also contains the
|
||||
`convert_one()` and `conver_many()` methods for converting MongoDB cursors to Python dicts.
|
||||
"""
|
||||
import json
|
||||
|
||||
import pymongo
|
||||
from app.db import AlbumMethods
|
||||
from app.db import ArtistMethods
|
||||
from app.db import PlaylistMethods
|
||||
from app.db import TrackMethods
|
||||
from bson import json_util
|
||||
|
||||
|
||||
class Mongo:
|
||||
"""
|
||||
The base class for all mongodb classes.
|
||||
"""
|
||||
|
||||
def __init__(self, database):
|
||||
mongo_uri = pymongo.MongoClient()
|
||||
self.db = mongo_uri[database]
|
||||
|
||||
|
||||
class MongoAlbums(Mongo, AlbumMethods):
|
||||
|
||||
def __init__(self):
|
||||
super(MongoAlbums, self).__init__("ALICE_ALBUMS")
|
||||
self.collection = self.db["ALL_ALBUMS"]
|
||||
|
||||
|
||||
class MongoArtists(Mongo, ArtistMethods):
|
||||
|
||||
def __init__(self):
|
||||
super(MongoArtists, self).__init__("ALICE_ARTISTS")
|
||||
self.collection = self.db["ALL_ARTISTS"]
|
||||
|
||||
|
||||
class MongoPlaylists(Mongo, PlaylistMethods):
|
||||
|
||||
def __init__(self):
|
||||
super(MongoPlaylists, self).__init__("ALICE_PLAYLISTS")
|
||||
self.collection = self.db["ALL_PLAYLISTS"]
|
||||
|
||||
|
||||
class MongoTracks(Mongo, TrackMethods):
|
||||
|
||||
def __init__(self):
|
||||
super(MongoTracks, self).__init__("ALICE_MUSIC_TRACKS")
|
||||
self.collection = self.db["ALL_TRACKS"]
|
||||
|
||||
|
||||
# ====================================================================== #
|
||||
# cursor convertion methods
|
||||
|
||||
|
||||
def convert_one(song):
|
||||
"""
|
||||
Converts a single mongodb cursor to a json object.
|
||||
"""
|
||||
json_song = json.dumps(song, default=json_util.default)
|
||||
loaded_song = json.loads(json_song)
|
||||
|
||||
return loaded_song
|
||||
|
||||
|
||||
def convert_many(array):
|
||||
"""
|
||||
Converts a list of mongodb cursors to a list of json objects.
|
||||
"""
|
||||
|
||||
songs = []
|
||||
|
||||
for song in array:
|
||||
json_song = json.dumps(song, default=json_util.default)
|
||||
loaded_song = json.loads(json_song)
|
||||
|
||||
songs.append(loaded_song)
|
||||
|
||||
return songs
|
||||
@@ -1,79 +0,0 @@
|
||||
"""
|
||||
This file contains the Album class for interacting with
|
||||
album documents in MongoDB.
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
from app.db.mongodb import MongoAlbums
|
||||
from app.models import Album
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
class Albums(MongoAlbums):
|
||||
"""
|
||||
The class for all album-related database operations.
|
||||
"""
|
||||
|
||||
def insert_album(self, album: Album) -> None:
|
||||
"""
|
||||
Inserts a new album object into the database.
|
||||
"""
|
||||
album = album.__dict__
|
||||
return self.collection.update_one(
|
||||
{
|
||||
"album": album["title"],
|
||||
"artist": album["artist"]
|
||||
},
|
||||
{
|
||||
"$set": album
|
||||
},
|
||||
upsert=True,
|
||||
).upserted_id
|
||||
|
||||
def insert_many(self, albums: Album):
|
||||
albums = [a.__dict__ for a in albums]
|
||||
"""
|
||||
Inserts multiple albums into the database.
|
||||
"""
|
||||
return self.collection.insert_many(albums)
|
||||
|
||||
def get_all_albums(self) -> list:
|
||||
"""
|
||||
Returns all the albums in the database.
|
||||
"""
|
||||
albums = self.collection.find()
|
||||
return convert_many(albums)
|
||||
|
||||
def get_album_by_id(self, id: str) -> dict:
|
||||
"""
|
||||
Returns a single album matching the id in the query params.
|
||||
"""
|
||||
album = self.collection.find_one({"_id": ObjectId(id)})
|
||||
return convert_one(album)
|
||||
|
||||
def get_album_by_name(self, name: str, artist: str) -> dict:
|
||||
"""
|
||||
Returns a single album matching the name in the query params.
|
||||
"""
|
||||
album = self.collection.find_one({"album": name, "artist": artist})
|
||||
return convert_one(album)
|
||||
|
||||
def find_album_by_hash(self, hash: str) -> dict:
|
||||
"""
|
||||
Returns a single album matching the hash in the query params.
|
||||
"""
|
||||
album = self.collection.find_one({"hash": hash})
|
||||
return convert_one(album)
|
||||
|
||||
def set_album_colors(self, colors: List[str], hash: str) -> None:
|
||||
"""
|
||||
Sets the colors for an album.
|
||||
"""
|
||||
self.collection.update_one(
|
||||
{"hash": hash},
|
||||
{"$set": {
|
||||
"colors": colors
|
||||
}},
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
"""
|
||||
This file contains the Artists class for interacting with artist documents in MongoDB.
|
||||
"""
|
||||
from app.db.mongodb import MongoArtists
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
class Artists(MongoArtists):
|
||||
"""
|
||||
The artist class for all artist related database operations.
|
||||
"""
|
||||
|
||||
def insert_artist(self, artist_obj: dict) -> None:
|
||||
"""
|
||||
Inserts an artist into the database.
|
||||
"""
|
||||
self.collection.update_one(artist_obj, {
|
||||
"$set": artist_obj
|
||||
},
|
||||
upsert=True).upserted_id
|
||||
|
||||
def get_all_artists(self) -> list:
|
||||
"""
|
||||
Returns a list of all artists in the database.
|
||||
"""
|
||||
return self.collection.find()
|
||||
|
||||
def get_artist_by_id(self, artist_id: str) -> dict:
|
||||
"""
|
||||
Returns an artist matching the mongo Id.
|
||||
"""
|
||||
return self.collection.find_one({"_id": ObjectId(artist_id)})
|
||||
|
||||
def get_artists_by_name(self, query: str):
|
||||
"""
|
||||
Returns all the artists matching the query.
|
||||
"""
|
||||
return self.collection.find({"name": query}).limit(20)
|
||||
@@ -1,85 +0,0 @@
|
||||
"""
|
||||
This file contains the Playlists class for interacting with the playlist documents in MongoDB.
|
||||
"""
|
||||
from app import helpers
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
from app.db.mongodb import MongoPlaylists
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
class Playlists(MongoPlaylists):
|
||||
"""
|
||||
The class for all playlist-related database operations.
|
||||
"""
|
||||
|
||||
def insert_playlist(self, playlist: dict) -> None:
|
||||
"""
|
||||
Inserts a new playlist object into the database.
|
||||
"""
|
||||
return self.collection.update_one(
|
||||
{
|
||||
"name": playlist["name"]
|
||||
},
|
||||
{
|
||||
"$set": playlist
|
||||
},
|
||||
upsert=True,
|
||||
).upserted_id
|
||||
|
||||
def get_all_playlists(self) -> list:
|
||||
"""
|
||||
Returns all the playlists in the database.
|
||||
"""
|
||||
playlists = self.collection.find()
|
||||
return convert_many(playlists)
|
||||
|
||||
def get_playlist_by_id(self, id: str) -> dict:
|
||||
"""
|
||||
Returns a single playlist matching the id in the query params.
|
||||
"""
|
||||
playlist = self.collection.find_one({"_id": ObjectId(id)})
|
||||
return convert_one(playlist)
|
||||
|
||||
def set_last_updated(self, playlistid: str) -> None:
|
||||
"""
|
||||
Sets the lastUpdated field to the current date.
|
||||
"""
|
||||
date = helpers.create_new_date()
|
||||
|
||||
return self.collection.update_one(
|
||||
{"_id": ObjectId(playlistid)},
|
||||
{"$set": {
|
||||
"lastUpdated": date
|
||||
}},
|
||||
)
|
||||
|
||||
def add_track_to_playlist(self, playlistid: str, track: dict) -> None:
|
||||
"""
|
||||
Adds a track to a playlist.
|
||||
"""
|
||||
self.collection.update_one(
|
||||
{
|
||||
"_id": ObjectId(playlistid),
|
||||
},
|
||||
{"$push": {
|
||||
"pre_tracks": track
|
||||
}},
|
||||
)
|
||||
self.set_last_updated(playlistid)
|
||||
|
||||
def get_playlist_by_name(self, name: str) -> dict:
|
||||
"""
|
||||
Returns a single playlist matching the name in the query params.
|
||||
"""
|
||||
playlist = self.collection.find_one({"name": name})
|
||||
return convert_one(playlist)
|
||||
|
||||
def update_playlist(self, playlistid: str, playlist: dict) -> None:
|
||||
"""
|
||||
Updates a playlist.
|
||||
"""
|
||||
return self.collection.update_one(
|
||||
{"_id": ObjectId(playlistid)},
|
||||
{"$set": playlist},
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
# """
|
||||
# This file contains the TrackColors class for interacting with Track colors documents in MongoDB.
|
||||
# """
|
||||
# from app import db
|
||||
# class TrackColors(db.Mongo):
|
||||
# """
|
||||
# The class for all track-related database operations.
|
||||
# """
|
||||
# def __init__(self):
|
||||
# super(TrackColors, self).__init__("ALICE_TRACK_COLORS")
|
||||
# self.collection = self.db["TRACK_COLORS"]
|
||||
# def insert_track_color(self, track_color: dict) -> None:
|
||||
# """
|
||||
# Inserts a new track object into the database.
|
||||
# """
|
||||
# return self.collection.update_one(
|
||||
# {
|
||||
# "filepath": track_color["filepath"]
|
||||
# },
|
||||
# {
|
||||
# "$set": track_color
|
||||
# },
|
||||
# upsert=True,
|
||||
# ).upserted_id
|
||||
# def get_track_color_by_track(self, filepath: str) -> dict:
|
||||
# """
|
||||
# Returns a track color object by its filepath.
|
||||
# """
|
||||
# track_color = self.collection.find_one({"filepath": filepath})
|
||||
# return db.convert_one(track_color)
|
||||
@@ -1,191 +0,0 @@
|
||||
"""
|
||||
This file contains the AllSongs class for interacting with track documents in MongoDB.
|
||||
"""
|
||||
import re
|
||||
|
||||
import pymongo
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
from app.db.mongodb import MongoTracks
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
class Tracks(MongoTracks):
|
||||
"""
|
||||
The class for all track-related database operations.
|
||||
"""
|
||||
|
||||
# def drop_db(self):
|
||||
# self.collection.drop()
|
||||
|
||||
def insert_song(self, song_obj: dict) -> str:
|
||||
"""
|
||||
Inserts a new track object into the database.
|
||||
"""
|
||||
return self.collection.update_one({
|
||||
"filepath": song_obj["filepath"]
|
||||
}, {
|
||||
"$set": song_obj
|
||||
},
|
||||
upsert=True).upserted_id
|
||||
|
||||
def insert_many(self, songs: list):
|
||||
"""
|
||||
Inserts multiple songs into the database.
|
||||
"""
|
||||
return self.collection.insert_many(songs)
|
||||
|
||||
def get_all_tracks(self) -> list:
|
||||
"""
|
||||
Returns all tracks in the database.
|
||||
"""
|
||||
return convert_many(self.collection.find())
|
||||
|
||||
def get_track_by_id(self, id: str) -> dict:
|
||||
"""
|
||||
Returns a track object by its mongodb id.
|
||||
"""
|
||||
song = self.collection.find_one({"_id": ObjectId(id)})
|
||||
return convert_one(song)
|
||||
|
||||
def get_song_by_album(self, name: str, artist: str) -> dict:
|
||||
"""
|
||||
Returns a single track matching the album in the query params.
|
||||
"""
|
||||
song = self.collection.find_one({"album": name, "albumartist": artist})
|
||||
return convert_one(song)
|
||||
|
||||
def search_songs_by_album(self, query: str) -> list:
|
||||
"""
|
||||
Returns all the songs matching the albums in the query params (using regex).
|
||||
"""
|
||||
songs = self.collection.find(
|
||||
{"album": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def search_songs_by_artist(self, query: str) -> list:
|
||||
"""
|
||||
Returns all the songs matching the artists in the query params.
|
||||
"""
|
||||
songs = self.collection.find(
|
||||
{"artists": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_song_by_title(self, query: str) -> list:
|
||||
"""
|
||||
Finds all the tracks matching the title in the query params.
|
||||
"""
|
||||
song = self.collection.find(
|
||||
{"title": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(song)
|
||||
|
||||
def find_songs_by_album(self, name: str, artist: str) -> list:
|
||||
"""
|
||||
Returns all the tracks exactly matching the album in the query params.
|
||||
"""
|
||||
songs = self.collection.find({"album": name, "albumartist": artist})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_songs_by_folder(self, query: str) -> list:
|
||||
"""
|
||||
Returns a sorted list of all the tracks exactly matching the folder in the query params
|
||||
"""
|
||||
songs = self.collection.find({
|
||||
"folder": query
|
||||
}).sort("title", pymongo.ASCENDING)
|
||||
return convert_many(songs)
|
||||
|
||||
def find_songs_by_filenames(self, filenames: list) -> list:
|
||||
"""
|
||||
Returns a list of all the tracks matching the filenames in the query params.
|
||||
"""
|
||||
songs = self.collection.find({"filepath": {"$in": filenames}})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_songs_by_folder_og(self, query: str) -> list:
|
||||
"""
|
||||
Returns an unsorted list of all the track matching the folder in the query params
|
||||
"""
|
||||
songs = self.collection.find({"folder": query})
|
||||
return convert_many(songs)
|
||||
|
||||
def get_dir_t_count(self, path: str) -> int:
|
||||
"""
|
||||
Returns a list of all the tracks matching the path in the query params.
|
||||
"""
|
||||
regex = re.compile(r"^.*" + re.escape(path) + r".*$")
|
||||
|
||||
return self.collection.count_documents({"filepath": {"$regex": regex}})
|
||||
|
||||
def find_songs_by_artist(self, query: str) -> list:
|
||||
"""
|
||||
Returns a list of all the tracks exactly matching the artists in the query params.
|
||||
"""
|
||||
songs = self.collection.find({"artists": query})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_songs_by_albumartist(self, query: str):
|
||||
"""
|
||||
Returns a list of all the tracks containing the albumartist in the query params.
|
||||
"""
|
||||
songs = self.collection.find(
|
||||
{"albumartist": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def get_song_by_path(self, path: str) -> dict:
|
||||
"""
|
||||
Returns a single track matching the filepath in the query params.
|
||||
"""
|
||||
song = self.collection.find_one({"filepath": path})
|
||||
return convert_one(song)
|
||||
|
||||
def remove_song_by_filepath(self, filepath: str):
|
||||
"""
|
||||
Removes a single track from the database. Returns a boolean indicating success or failure of the operation.
|
||||
"""
|
||||
try:
|
||||
self.collection.delete_one({"filepath": filepath})
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def remove_song_by_id(self, id: str):
|
||||
"""
|
||||
Removes a single track from the database. Returns a boolean indicating success or failure of the operation.
|
||||
"""
|
||||
try:
|
||||
self.collection.delete_one({"_id": ObjectId(id)})
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def find_tracks_by_hash(self, hash: str) -> list:
|
||||
"""
|
||||
Returns a list of all the tracks matching the hash in the query params.
|
||||
"""
|
||||
songs = self.collection.find({"albumhash": hash})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_track_by_title_artists_album(self, title: str, artist: str,
|
||||
album: str) -> dict:
|
||||
"""
|
||||
Returns a single track matching the title, artist, and album in the query params.
|
||||
"""
|
||||
song = self.collection.find_one({
|
||||
"title": title,
|
||||
"artists": artist,
|
||||
"album": album
|
||||
})
|
||||
return convert_one(song)
|
||||
@@ -1,14 +0,0 @@
|
||||
class TrackExistsInPlaylistError(Exception):
|
||||
"""
|
||||
Exception raised when a track is already in a playlist.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PlaylistExistsError(Exception):
|
||||
"""
|
||||
Exception raised when a playlist already exists.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -1,180 +0,0 @@
|
||||
"""
|
||||
This module contains functions for the server
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
from app import helpers
|
||||
from app import settings
|
||||
from app.lib import trackslib
|
||||
from app.lib import watchdoge
|
||||
from app.lib.albumslib import ValidateAlbumThumbs
|
||||
from app.lib.colorlib import ProcessAlbumColors
|
||||
from app.lib.playlistlib import ValidatePlaylistThumbs
|
||||
from app.lib.populate import CreateAlbums
|
||||
from app.lib.populate import Populate
|
||||
from app.logger import get_logger
|
||||
from PIL import Image
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
@helpers.background
|
||||
def run_checks():
|
||||
"""
|
||||
Checks for new songs every 5 minutes.
|
||||
"""
|
||||
ValidateAlbumThumbs()
|
||||
ValidatePlaylistThumbs()
|
||||
|
||||
while True:
|
||||
trackslib.validate_tracks()
|
||||
|
||||
Populate()
|
||||
CreateAlbums()
|
||||
ProcessAlbumColors()
|
||||
|
||||
if helpers.Ping()():
|
||||
CheckArtistImages()()
|
||||
|
||||
time.sleep(300)
|
||||
|
||||
|
||||
@helpers.background
|
||||
def start_watchdog():
|
||||
"""
|
||||
Starts the file watcher.
|
||||
"""
|
||||
watchdoge.watch.run()
|
||||
|
||||
|
||||
class getArtistImage:
|
||||
"""
|
||||
Returns an artist image url.
|
||||
"""
|
||||
|
||||
def __init__(self, artist: str):
|
||||
self.artist = artist
|
||||
|
||||
def __call__(self):
|
||||
try:
|
||||
url = f"https://api.deezer.com/search/artist?q={self.artist}"
|
||||
response = requests.get(url)
|
||||
data = response.json()
|
||||
|
||||
return data["data"][0]["picture_medium"]
|
||||
except requests.exceptions.ConnectionError:
|
||||
time.sleep(5)
|
||||
return None
|
||||
except (IndexError, KeyError):
|
||||
return None
|
||||
|
||||
|
||||
class useImageDownloader:
|
||||
def __init__(self, url: str, dest: str) -> None:
|
||||
self.url = url
|
||||
self.dest = dest
|
||||
|
||||
def __call__(self) -> None:
|
||||
try:
|
||||
img = Image.open(BytesIO(requests.get(self.url).content))
|
||||
img.save(self.dest, format="webp")
|
||||
img.close()
|
||||
return "fetched image"
|
||||
except requests.exceptions.ConnectionError:
|
||||
time.sleep(5)
|
||||
return "connection error"
|
||||
|
||||
|
||||
class CheckArtistImages:
|
||||
def __init__(self):
|
||||
self.artists: list[str] = []
|
||||
log.info("Checking artist images")
|
||||
|
||||
@staticmethod
|
||||
def check_if_exists(img_path: str):
|
||||
"""
|
||||
Checks if an image exists on c.
|
||||
"""
|
||||
|
||||
if os.path.exists(img_path):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def download_image(cls, artistname: str):
|
||||
"""
|
||||
Checks if an artist image exists and downloads it if not.
|
||||
|
||||
:param artistname: The artist name
|
||||
"""
|
||||
|
||||
img_path = (
|
||||
settings.APP_DIR
|
||||
+ "/images/artists/"
|
||||
+ helpers.create_safe_name(artistname)
|
||||
+ ".webp"
|
||||
)
|
||||
|
||||
if cls.check_if_exists(img_path):
|
||||
return "exists"
|
||||
|
||||
url = getArtistImage(artistname)()
|
||||
|
||||
if url is None:
|
||||
return "url is none"
|
||||
|
||||
return useImageDownloader(url, img_path)()
|
||||
|
||||
def __call__(self):
|
||||
self.artists = helpers.Get.get_all_artists()
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
iter = pool.map(self.download_image, self.artists)
|
||||
[i for i in iter]
|
||||
|
||||
print("Done fetching images")
|
||||
|
||||
|
||||
def fetch_album_bio(title: str, albumartist: str) -> str | None:
|
||||
"""
|
||||
Returns the album bio for a given album.
|
||||
"""
|
||||
last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={}&format=json".format(
|
||||
settings.LAST_FM_API_KEY, albumartist, title
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.get(last_fm_url)
|
||||
data = response.json()
|
||||
except:
|
||||
return None
|
||||
|
||||
try:
|
||||
bio = data["album"]["wiki"]["summary"].split('<a href="https://www.last.fm/')[0]
|
||||
except KeyError:
|
||||
bio = None
|
||||
|
||||
return bio
|
||||
|
||||
|
||||
class FetchAlbumBio:
|
||||
"""
|
||||
Returns the album bio for a given album.
|
||||
"""
|
||||
|
||||
def __init__(self, title: str, albumartist: str):
|
||||
self.title = title
|
||||
self.albumartist = albumartist
|
||||
|
||||
def __call__(self):
|
||||
return fetch_album_bio(self.title, self.albumartist)
|
||||
|
||||
|
||||
# TODO
|
||||
# - Move the populate function to a new file and probably into a new class
|
||||
# - Start movement from functional programming to OOP to OOP
|
||||
@@ -1,226 +0,0 @@
|
||||
"""
|
||||
This module contains mini functions for the server.
|
||||
"""
|
||||
import os
|
||||
from pprint import pprint
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Set
|
||||
|
||||
import requests
|
||||
|
||||
from app import instances, models
|
||||
|
||||
|
||||
def background(func):
|
||||
"""
|
||||
a threading decorator
|
||||
use @background above the function you want to run in the background
|
||||
"""
|
||||
|
||||
def background_func(*a, **kw):
|
||||
threading.Thread(target=func, args=a, kwargs=kw).start()
|
||||
|
||||
return background_func
|
||||
|
||||
|
||||
def run_fast_scandir(__dir: str, 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.
|
||||
"""
|
||||
|
||||
subfolders = []
|
||||
files = []
|
||||
ext = [".flac", ".mp3"]
|
||||
|
||||
for f in os.scandir(__dir):
|
||||
if f.is_dir() and not f.name.startswith("."):
|
||||
subfolders.append(f.path)
|
||||
if f.is_file():
|
||||
if os.path.splitext(f.name)[1].lower() in ext:
|
||||
files.append(f.path)
|
||||
|
||||
if full or len(files) == 0:
|
||||
for _dir in list(subfolders):
|
||||
sf, f = run_fast_scandir(_dir, full=True)
|
||||
subfolders.extend(sf)
|
||||
files.extend(f)
|
||||
|
||||
return subfolders, files
|
||||
|
||||
|
||||
class RemoveDuplicates:
|
||||
def __init__(self, tracklist: List[models.Track]) -> None:
|
||||
self.tracklist = tracklist
|
||||
|
||||
def __call__(self) -> List[models.Track]:
|
||||
uniq_hashes = []
|
||||
[
|
||||
uniq_hashes.append(t.uniq_hash)
|
||||
for t in self.tracklist
|
||||
if t.uniq_hash not in uniq_hashes
|
||||
]
|
||||
tracks = UseBisection(self.tracklist, "uniq_hash", uniq_hashes)()
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
def is_valid_file(filename: str) -> bool:
|
||||
"""
|
||||
Checks if a file is valid. Returns True if it is, False if it isn't.
|
||||
"""
|
||||
|
||||
if filename.endswith(".flac") or filename.endswith(".mp3"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_hash(*args: List[str]) -> str:
|
||||
"""
|
||||
Creates a simple hash for an album
|
||||
"""
|
||||
string = "".join(a for a in args).replace(" ", "")
|
||||
return "".join([i for i in string if i.isalnum()]).lower()
|
||||
|
||||
|
||||
def create_new_date():
|
||||
now = datetime.now()
|
||||
str = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
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.isalnum()]).lower()
|
||||
|
||||
|
||||
class UseBisection:
|
||||
"""
|
||||
Uses bisection to find a list of items in another list.
|
||||
|
||||
returns a list of found items with `None` items being not found
|
||||
items.
|
||||
"""
|
||||
|
||||
def __init__(self, source: List, search_from: str, queries: List[str]) -> None:
|
||||
self.source_list = source
|
||||
self.queries_list = queries
|
||||
self.attr = search_from
|
||||
self.source_list.sort(key=lambda x: getattr(x, search_from))
|
||||
|
||||
def find(self, query: str):
|
||||
left = 0
|
||||
right = len(self.source_list) - 1
|
||||
|
||||
while left <= right:
|
||||
mid = (left + right) // 2
|
||||
if self.source_list[mid].__getattribute__(self.attr) == query:
|
||||
return self.source_list[mid]
|
||||
elif self.source_list[mid].__getattribute__(self.attr) > query:
|
||||
right = mid - 1
|
||||
else:
|
||||
left = mid + 1
|
||||
|
||||
return None
|
||||
|
||||
def __call__(self) -> List:
|
||||
if len(self.source_list) == 0:
|
||||
print("🚀🚀🚀🚀🚀🚀🚀")
|
||||
return [None]
|
||||
|
||||
return [self.find(query) for query in self.queries_list]
|
||||
|
||||
|
||||
class Get:
|
||||
@staticmethod
|
||||
def get_all_tracks() -> List[models.Track]:
|
||||
"""
|
||||
Returns all tracks
|
||||
"""
|
||||
t = instances.tracks_instance.get_all_tracks()
|
||||
return [models.Track(t) for t in t]
|
||||
|
||||
def get_all_albums() -> List[models.Album]:
|
||||
"""
|
||||
Returns all albums
|
||||
"""
|
||||
a = instances.album_instance.get_all_albums()
|
||||
return [models.Album(a) for a in a]
|
||||
|
||||
@classmethod
|
||||
def get_all_artists(cls) -> Set[str]:
|
||||
tracks = cls.get_all_tracks()
|
||||
artists: Set[str] = set()
|
||||
|
||||
for track in tracks:
|
||||
for artist in track.artists:
|
||||
artists.add(artist)
|
||||
|
||||
return artists
|
||||
|
||||
@staticmethod
|
||||
def get_all_playlists() -> List[models.Playlist]:
|
||||
"""
|
||||
Returns all playlists
|
||||
"""
|
||||
p = instances.playlist_instance.get_all_playlists()
|
||||
return [models.Playlist(p) for p in p]
|
||||
|
||||
|
||||
class Ping:
|
||||
"""Checks if there is a connection to the internet by pinging google.com"""
|
||||
|
||||
@staticmethod
|
||||
def __call__() -> bool:
|
||||
try:
|
||||
requests.get("https://google.com", timeout=10)
|
||||
return True
|
||||
except (requests.exceptions.ConnectionError, requests.Timeout):
|
||||
return False
|
||||
|
||||
|
||||
def get_normal_artist_name(artists: List[str]) -> str:
|
||||
"""
|
||||
Returns the artist name with most capital letters.
|
||||
"""
|
||||
if len(artists) == 1:
|
||||
return artists[0]
|
||||
|
||||
artists.sort()
|
||||
return artists[0]
|
||||
|
||||
|
||||
def get_artist_lists(artists: List[str]) -> List[str]:
|
||||
"""
|
||||
Takes in a list of artists and returns a list of lists of an artist's various name variations.
|
||||
|
||||
Example:
|
||||
>>> get_artist_lists(['Juice WRLD', 'Juice Wrld', 'XXXtentacion', 'XXXTENTACION'])
|
||||
|
||||
>>> [['Juice WRLD', 'Juice Wrld'], ['XXXtentacion', 'XXXTENTACION']]
|
||||
"""
|
||||
artist_lists: List[List[str]] = []
|
||||
|
||||
for artist in artists:
|
||||
for list in artist_lists:
|
||||
if artist.lower() == list[0].lower():
|
||||
list.append(artist)
|
||||
break
|
||||
else:
|
||||
artist_lists.append([artist])
|
||||
|
||||
return artist_lists
|
||||
|
||||
|
||||
def get_normalized_artists(names: List[str]) -> List[models.Artist]:
|
||||
"""
|
||||
Takes in a list of artists and returns a list of models.Artist objects with normalized names.
|
||||
"""
|
||||
names = [n.strip() for n in names]
|
||||
names = get_artist_lists(names)
|
||||
names = [get_normal_artist_name(a) for a in names]
|
||||
|
||||
return [models.Artist(a) for a in names]
|
||||
@@ -1,73 +0,0 @@
|
||||
from os import path
|
||||
from typing import Tuple
|
||||
|
||||
from flask import Flask
|
||||
from flask import send_from_directory
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def join(*args: Tuple[str]) -> str:
|
||||
return path.join(*args)
|
||||
|
||||
|
||||
HOME = path.expanduser("~")
|
||||
APP_DIR = join(HOME, ".alice")
|
||||
IMG_PATH = path.join(APP_DIR, "images")
|
||||
|
||||
ASSETS_PATH = join(APP_DIR, "assets")
|
||||
THUMB_PATH = join(IMG_PATH, "thumbnails")
|
||||
ARTIST_PATH = join(IMG_PATH, "artists")
|
||||
PLAYLIST_PATH = join(IMG_PATH, "playlists")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def hello():
|
||||
return "Hello mf"
|
||||
|
||||
|
||||
def send_fallback_img():
|
||||
img = join(ASSETS_PATH, "default.webp")
|
||||
exists = path.exists(img)
|
||||
|
||||
if not exists:
|
||||
return "", 404
|
||||
|
||||
return send_from_directory(ASSETS_PATH, "default.webp")
|
||||
|
||||
|
||||
@app.route("/t/<imgpath>")
|
||||
def send_thumbnail(imgpath: str):
|
||||
fpath = join(THUMB_PATH, imgpath)
|
||||
exists = path.exists(fpath)
|
||||
|
||||
if exists:
|
||||
return send_from_directory(THUMB_PATH, imgpath)
|
||||
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
@app.route("/a/<imgpath>")
|
||||
def send_artist_image(imgpath: str):
|
||||
fpath = join(ARTIST_PATH, imgpath)
|
||||
exists = path.exists(fpath)
|
||||
|
||||
if exists:
|
||||
return send_from_directory(ARTIST_PATH, imgpath)
|
||||
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
@app.route("/p/<imgpath>")
|
||||
def send_playlist_image(imgpath: str):
|
||||
fpath = join(PLAYLIST_PATH, imgpath)
|
||||
exists = path.exists(fpath)
|
||||
|
||||
if exists:
|
||||
return send_from_directory(PLAYLIST_PATH, imgpath)
|
||||
|
||||
return send_fallback_img()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(threaded=True, port=9877)
|
||||
@@ -1,12 +0,0 @@
|
||||
"""
|
||||
All the MongoDB instances are created here.
|
||||
"""
|
||||
from app.db.mongodb import albums
|
||||
from app.db.mongodb import artists
|
||||
from app.db.mongodb import playlists
|
||||
from app.db.mongodb import tracks
|
||||
|
||||
tracks_instance = tracks.Tracks()
|
||||
artist_instance = artists.Artists()
|
||||
album_instance = albums.Albums()
|
||||
playlist_instance = playlists.Playlists()
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
This module contains all the data processing and non-API libraries
|
||||
"""
|
||||
@@ -1,158 +0,0 @@
|
||||
"""
|
||||
This library contains all the functions related to albums.
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.lib import taglib
|
||||
from app.logger import logg
|
||||
from app.settings import THUMBS_PATH
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
@dataclass
|
||||
class Thumbnail:
|
||||
filename: str
|
||||
|
||||
|
||||
class RipAlbumImage:
|
||||
"""
|
||||
Rips a thumbnail for the given album hash.
|
||||
"""
|
||||
|
||||
def __init__(self, hash: str) -> None:
|
||||
tracks = instances.tracks_instance.find_tracks_by_hash(hash)
|
||||
tracks = [models.Track(track) for track in tracks]
|
||||
|
||||
for track in tracks:
|
||||
ripped = taglib.extract_thumb(track.filepath, hash + ".webp")
|
||||
|
||||
if ripped:
|
||||
break
|
||||
|
||||
|
||||
class ValidateAlbumThumbs:
|
||||
@staticmethod
|
||||
def remove_obsolete():
|
||||
"""
|
||||
Removes unreferenced thumbnails from the thumbnails folder.
|
||||
"""
|
||||
entries = os.scandir(THUMBS_PATH)
|
||||
entries = [entry for entry in entries if entry.is_file()]
|
||||
|
||||
albums = helpers.Get.get_all_albums()
|
||||
thumbs = [Thumbnail(album.hash + ".webp") for album in albums]
|
||||
|
||||
for entry in tqdm(entries, desc="Validating thumbnails"):
|
||||
e = helpers.UseBisection(thumbs, "filename", [entry.name])()
|
||||
|
||||
if e is None:
|
||||
os.remove(entry.path)
|
||||
break
|
||||
|
||||
if os.path.getsize(entry.path) == 0:
|
||||
os.remove(entry.path)
|
||||
|
||||
@staticmethod
|
||||
def find_lost_thumbnails():
|
||||
"""
|
||||
Re-rip lost album thumbnails
|
||||
"""
|
||||
entries = os.scandir(THUMBS_PATH)
|
||||
entries = [Thumbnail(entry.name) for entry in entries if entry.is_file()]
|
||||
|
||||
albums = helpers.Get.get_all_albums()
|
||||
thumbs = [(album.hash + ".webp") for album in albums]
|
||||
|
||||
def rip_image(t_hash: str):
|
||||
e = helpers.UseBisection(entries, "filename", [t_hash])()[0]
|
||||
|
||||
if e is None:
|
||||
hash = t_hash.replace(".webp", "")
|
||||
RipAlbumImage(hash)
|
||||
|
||||
logg.info("Ripping lost album thumbnails")
|
||||
# with ThreadPoolExecutor() as pool:
|
||||
# i = pool.map(rip_image, thumbs)
|
||||
# [a for a in i]
|
||||
# ⚠️ empty lists are sent to the useBisection function as the source list.
|
||||
for thumb in thumbs:
|
||||
rip_image(thumb)
|
||||
|
||||
logg.info("Ripping lost album thumbnails ... ✅")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.remove_obsolete()
|
||||
self.find_lost_thumbnails()
|
||||
|
||||
|
||||
def use_defaults() -> str:
|
||||
"""
|
||||
Returns a path to a random image in the defaults directory.
|
||||
"""
|
||||
path = "defaults/" + str(random.randint(0, 20)) + ".webp"
|
||||
return path
|
||||
|
||||
|
||||
def get_album_image(track: models.Track) -> str:
|
||||
"""
|
||||
Gets the image of an album.
|
||||
"""
|
||||
|
||||
img_p = track.albumhash + ".webp"
|
||||
|
||||
success = taglib.extract_thumb(track.filepath, webp_path=img_p)
|
||||
|
||||
if success:
|
||||
return img_p
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class GetAlbumTracks:
|
||||
"""
|
||||
Finds all the tracks that match a specific album, given the album title
|
||||
and album artist.
|
||||
"""
|
||||
|
||||
def __init__(self, tracklist: List[models.Track], albumhash: str) -> None:
|
||||
self.hash = albumhash
|
||||
self.tracks = tracklist
|
||||
self.tracks.sort(key=lambda x: x.albumhash)
|
||||
|
||||
def __call__(self):
|
||||
tracks = helpers.UseBisection(self.tracks, "albumhash", [self.hash])()
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
def get_album_tracks(tracklist: List[models.Track], hash: str) -> List:
|
||||
return GetAlbumTracks(tracklist, hash)()
|
||||
|
||||
|
||||
def create_album(track: models.Track) -> dict:
|
||||
"""
|
||||
Generates and returns an album object from a track object.
|
||||
"""
|
||||
album = {
|
||||
"title": track.album,
|
||||
"artist": track.albumartist,
|
||||
"hash": track.albumhash,
|
||||
"copyright": track.copyright,
|
||||
}
|
||||
|
||||
album["date"] = track.date
|
||||
|
||||
img_p = get_album_image(track)
|
||||
|
||||
if img_p is not None:
|
||||
album["image"] = img_p
|
||||
return album
|
||||
|
||||
album["image"] = None
|
||||
return album
|
||||
@@ -1,49 +0,0 @@
|
||||
import colorgram
|
||||
from app import instances
|
||||
from app import settings
|
||||
from app.helpers import Get
|
||||
from app.logger import get_logger
|
||||
from app.models import Album
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
def get_image_colors(image: str) -> list:
|
||||
"""Extracts 2 of the most dominant colors from an image."""
|
||||
try:
|
||||
colors = sorted(colorgram.extract(image, 4), key=lambda c: c.hsl.h)
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
formatted_colors = []
|
||||
|
||||
for color in colors:
|
||||
color = f"rgb({color.rgb.r}, {color.rgb.g}, {color.rgb.b})"
|
||||
formatted_colors.append(color)
|
||||
|
||||
return formatted_colors
|
||||
|
||||
|
||||
class ProcessAlbumColors:
|
||||
|
||||
def __init__(self) -> None:
|
||||
log.info("Processing album colors")
|
||||
all_albums = Get.get_all_albums()
|
||||
|
||||
all_albums = [a for a in all_albums if len(a.colors) == 0]
|
||||
|
||||
for a in all_albums:
|
||||
self.process_color(a)
|
||||
|
||||
log.info("Processing album colors ... ✅")
|
||||
|
||||
@staticmethod
|
||||
def process_color(album: Album):
|
||||
img = settings.THUMBS_PATH + "/" + album.image
|
||||
|
||||
colors = get_image_colors(img)
|
||||
|
||||
if len(colors) > 0:
|
||||
instances.album_instance.set_album_colors(colors, album.hash)
|
||||
|
||||
return colors
|
||||
@@ -1,73 +0,0 @@
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from os import scandir
|
||||
from typing import Tuple
|
||||
|
||||
from app import instances
|
||||
from app.models import Folder
|
||||
from app.models import Track
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dir:
|
||||
path: str
|
||||
is_sym: bool
|
||||
|
||||
|
||||
def get_folder_track_count(path: str) -> int:
|
||||
"""
|
||||
Returns the number of files associated with a folder.
|
||||
"""
|
||||
tracks = instances.tracks_instance.get_dir_t_count(path)
|
||||
return len(tracks)
|
||||
|
||||
|
||||
def create_folder(dir: Dir) -> Folder:
|
||||
"""Create a single Folder object"""
|
||||
folder = {
|
||||
"name": dir.path.split("/")[-1],
|
||||
"path": dir.path,
|
||||
"is_sym": dir.is_sym,
|
||||
"trackcount": instances.tracks_instance.get_dir_t_count(dir.path),
|
||||
}
|
||||
|
||||
return Folder(folder)
|
||||
|
||||
|
||||
class getFnF:
|
||||
"""
|
||||
Get files and folders from a directory.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path = path
|
||||
|
||||
def __call__(self) -> Tuple[Track, Folder]:
|
||||
try:
|
||||
all = scandir(self.path)
|
||||
except FileNotFoundError:
|
||||
return ([], [])
|
||||
|
||||
dirs, files = [], []
|
||||
|
||||
for entry in all:
|
||||
if entry.is_dir() and not entry.name.startswith("."):
|
||||
dir = {
|
||||
"path": entry.path,
|
||||
"is_sym": entry.is_symlink(),
|
||||
}
|
||||
dirs.append(Dir(**dir))
|
||||
elif entry.is_file() and entry.name.endswith((".mp3", ".flac")):
|
||||
files.append(entry.path)
|
||||
|
||||
tracks = instances.tracks_instance.find_songs_by_filenames(files)
|
||||
tracks = [Track(track) for track in tracks]
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
iter = pool.map(create_folder, dirs)
|
||||
folders = [i for i in iter if i is not None]
|
||||
|
||||
folders = filter(lambda f: f.trackcount > 0, folders)
|
||||
|
||||
return tracks, folders
|
||||
@@ -1,157 +0,0 @@
|
||||
"""
|
||||
This library contains all the functions related to playlists.
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from app import exceptions
|
||||
from app import instances
|
||||
from app import models
|
||||
from app import settings
|
||||
from app.helpers import Get, get_normalized_artists
|
||||
from app.lib import trackslib
|
||||
from app.logger import get_logger
|
||||
from PIL import Image
|
||||
from PIL import ImageSequence
|
||||
from werkzeug import datastructures
|
||||
|
||||
|
||||
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylistError
|
||||
|
||||
logg = get_logger()
|
||||
|
||||
|
||||
def add_track(playlistid: str, trackid: str):
|
||||
"""
|
||||
Adds a track to a playlist to the database.
|
||||
"""
|
||||
tt = instances.tracks_instance.get_track_by_id(trackid)
|
||||
|
||||
if tt is None:
|
||||
return
|
||||
|
||||
track = models.Track(tt)
|
||||
|
||||
playlist = instances.playlist_instance.get_playlist_by_id(playlistid)
|
||||
|
||||
track = {
|
||||
"title": track.title,
|
||||
"artists": tt["artists"],
|
||||
"album": track.album,
|
||||
}
|
||||
if track in playlist["pre_tracks"]:
|
||||
raise TrackExistsInPlaylist
|
||||
|
||||
instances.playlist_instance.add_track_to_playlist(playlistid, track)
|
||||
|
||||
|
||||
def create_thumbnail(image: any, img_path: str) -> str:
|
||||
"""
|
||||
Creates a 250 x 250 thumbnail from a playlist image
|
||||
"""
|
||||
thumb_path = "thumb_" + img_path
|
||||
full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists", thumb_path)
|
||||
|
||||
aspect_ratio = image.width / image.height
|
||||
|
||||
new_w = round(250 * aspect_ratio)
|
||||
|
||||
thumb = image.resize((new_w, 250), Image.ANTIALIAS)
|
||||
thumb.save(full_thumb_path, "webp")
|
||||
|
||||
return thumb_path
|
||||
|
||||
|
||||
def save_p_image(file: datastructures.FileStorage, pid: str):
|
||||
"""
|
||||
Saves the image of a playlist to the database.
|
||||
"""
|
||||
img = Image.open(file)
|
||||
|
||||
random_str = "".join(random.choices(string.ascii_letters + string.digits, k=5))
|
||||
|
||||
img_path = pid + str(random_str) + ".webp"
|
||||
|
||||
full_img_path = os.path.join(settings.APP_DIR, "images", "playlists", img_path)
|
||||
|
||||
if file.content_type == "image/gif":
|
||||
frames = []
|
||||
|
||||
for frame in ImageSequence.Iterator(img):
|
||||
frames.append(frame.copy())
|
||||
|
||||
frames[0].save(full_img_path, save_all=True, append_images=frames[1:])
|
||||
thumb_path = create_thumbnail(img, img_path=img_path)
|
||||
|
||||
return img_path, thumb_path
|
||||
|
||||
img.save(full_img_path, "webp")
|
||||
thumb_path = create_thumbnail(img, img_path=img_path)
|
||||
|
||||
return img_path, thumb_path
|
||||
|
||||
|
||||
class ValidatePlaylistThumbs:
|
||||
"""
|
||||
Removes all unused images in the images/playlists folder.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
images = []
|
||||
playlists = Get.get_all_playlists()
|
||||
|
||||
logg.info("Validating playlist thumbnails")
|
||||
for playlist in playlists:
|
||||
if playlist.image:
|
||||
img_path = playlist.image.split("/")[-1]
|
||||
thumb_path = playlist.thumb.split("/")[-1]
|
||||
|
||||
images.append(img_path)
|
||||
images.append(thumb_path)
|
||||
|
||||
p_path = os.path.join(settings.APP_DIR, "images", "playlists")
|
||||
|
||||
for image in os.listdir(p_path):
|
||||
if image not in images:
|
||||
os.remove(os.path.join(p_path, image))
|
||||
|
||||
logg.info("Validating playlist thumbnails ... ✅")
|
||||
|
||||
|
||||
def create_new_date():
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def create_playlist_tracks(playlist_tracks: List) -> List[models.Track]:
|
||||
"""
|
||||
Creates a list of model.Track objects from a list of playlist track dicts.
|
||||
"""
|
||||
tracks: List[models.Track] = []
|
||||
|
||||
for t in playlist_tracks:
|
||||
track = trackslib.get_p_track(t)
|
||||
|
||||
if track is not None:
|
||||
tracks.append(models.Track(track))
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
class GetPlaylistArtists:
|
||||
"""
|
||||
Returns a list of artists from a list of playlist tracks.
|
||||
"""
|
||||
|
||||
def __init__(self, pid: str) -> None:
|
||||
self.pid = pid
|
||||
p = instances.playlist_instance.get_playlist_by_id(self.pid)
|
||||
self.tracks = create_playlist_tracks(p["pre_tracks"])
|
||||
|
||||
def __call__(self):
|
||||
artists = set()
|
||||
|
||||
artists = [a for t in self.tracks for a in t.artists]
|
||||
return get_normalized_artists(artists)
|
||||
@@ -1,157 +0,0 @@
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from app import instances
|
||||
from app import settings
|
||||
from app.helpers import create_hash
|
||||
from app.helpers import Get
|
||||
from app.helpers import run_fast_scandir
|
||||
from app.helpers import UseBisection
|
||||
from app.instances import tracks_instance
|
||||
from app.lib.albumslib import create_album
|
||||
from app.lib.taglib import get_tags
|
||||
from app.logger import logg
|
||||
from app.models import Album
|
||||
from app.models import Track
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class Populate:
|
||||
"""
|
||||
Populate the database with all songs in the music directory
|
||||
|
||||
checks if the song is in the database, if not, it adds it
|
||||
also checks if the album art exists in the image path, if not tries to
|
||||
extract it.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.db_tracks = []
|
||||
self.tagged_tracks = []
|
||||
|
||||
self.files = run_fast_scandir(settings.HOME_DIR, full=True)[1]
|
||||
self.db_tracks = tracks_instance.get_all_tracks()
|
||||
|
||||
self.check_untagged()
|
||||
self.tag_untagged()
|
||||
|
||||
def check_untagged(self):
|
||||
"""
|
||||
Loops through all the tracks in db tracks removing each
|
||||
from the list of tagged tracks if it exists.
|
||||
We will now only have untagged tracks left in `files`.
|
||||
"""
|
||||
for track in tqdm(self.db_tracks, desc="Checking untagged"):
|
||||
if track["filepath"] in self.files:
|
||||
self.files.remove(track["filepath"])
|
||||
|
||||
def get_tags(self, file: str):
|
||||
tags = get_tags(file)
|
||||
|
||||
if tags is not None:
|
||||
hash = create_hash(tags["album"], tags["albumartist"])
|
||||
tags["albumhash"] = hash
|
||||
self.tagged_tracks.append(tags)
|
||||
|
||||
def tag_untagged(self):
|
||||
"""
|
||||
Loops through all the untagged files and tags them.
|
||||
"""
|
||||
|
||||
logg.info("Tagging untagged tracks...")
|
||||
with ThreadPoolExecutor() as executor:
|
||||
executor.map(self.get_tags, self.files)
|
||||
|
||||
if len(self.tagged_tracks) > 0:
|
||||
tracks_instance.insert_many(self.tagged_tracks)
|
||||
|
||||
logg.info(f"Tagged {len(self.tagged_tracks)} tracks.")
|
||||
|
||||
|
||||
@dataclass
|
||||
class PreAlbum:
|
||||
title: str
|
||||
artist: str
|
||||
hash: str
|
||||
|
||||
|
||||
class CreateAlbums:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.db_tracks = Get.get_all_tracks()
|
||||
self.db_albums = Get.get_all_albums()
|
||||
|
||||
prealbums = self.create_pre_albums(self.db_tracks)
|
||||
prealbums = self.filter_processed(self.db_albums, prealbums)
|
||||
|
||||
albums = []
|
||||
|
||||
for album in tqdm(prealbums, desc="Creating albums"):
|
||||
a = self.create_album(album)
|
||||
if a is not None:
|
||||
albums.append(a)
|
||||
|
||||
# with ThreadPoolExecutor() as pool:
|
||||
# iterator = pool.map(self.create_album, prealbums)
|
||||
|
||||
# for i in iterator:
|
||||
# if i is not None:
|
||||
# albums.append(i)
|
||||
|
||||
if len(albums) > 0:
|
||||
instances.album_instance.insert_many(albums)
|
||||
|
||||
@staticmethod
|
||||
def create_pre_albums(tracks: List[Track]) -> List[PreAlbum]:
|
||||
prealbums = []
|
||||
|
||||
for track in tqdm(tracks, desc="Creating prealbums"):
|
||||
album = {
|
||||
"title": track.album,
|
||||
"artist": track.albumartist,
|
||||
"hash": track.albumhash,
|
||||
}
|
||||
|
||||
album = PreAlbum(**album)
|
||||
|
||||
if album not in prealbums:
|
||||
prealbums.append(album)
|
||||
|
||||
return prealbums
|
||||
|
||||
@staticmethod
|
||||
def filter_processed(albums: List[Album],
|
||||
prealbums: List[PreAlbum]) -> List[dict]:
|
||||
to_process = []
|
||||
|
||||
for p in tqdm(prealbums, desc="Filtering processed albums"):
|
||||
album = UseBisection(albums, "hash", [p.hash])()[0]
|
||||
|
||||
if album is None:
|
||||
to_process.append(p)
|
||||
|
||||
return to_process
|
||||
|
||||
def create_album(self, album: PreAlbum) -> Album:
|
||||
hash = album.hash
|
||||
|
||||
album = {"image": None}
|
||||
iter = 0
|
||||
|
||||
while album["image"] is None:
|
||||
track = UseBisection(self.db_tracks, "albumhash", [hash])()[0]
|
||||
|
||||
if track is not None:
|
||||
iter += 1
|
||||
album = create_album(track)
|
||||
self.db_tracks.remove(track)
|
||||
else:
|
||||
album["image"] = hash + ".webp"
|
||||
try:
|
||||
album = Album(album)
|
||||
return album
|
||||
except KeyError:
|
||||
print(f"📌 {iter}")
|
||||
print(album)
|
||||
@@ -1,128 +0,0 @@
|
||||
"""
|
||||
This library contains all the functions related to the search functionality.
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Cutoff:
|
||||
"""
|
||||
Holds all the default cutoff values.
|
||||
"""
|
||||
|
||||
tracks: int = 80
|
||||
albums: int = 80
|
||||
artists: int = 80
|
||||
playlists: int = 80
|
||||
|
||||
|
||||
class Limit:
|
||||
"""
|
||||
Holds all the default limit values.
|
||||
"""
|
||||
|
||||
tracks: int = 50
|
||||
albums: int = 50
|
||||
artists: int = 50
|
||||
playlists: int = 50
|
||||
|
||||
|
||||
class SearchTracks:
|
||||
def __init__(self, tracks: List[models.Track], query: str) -> None:
|
||||
self.query = query
|
||||
self.tracks = tracks
|
||||
|
||||
def __call__(self) -> List[models.Track]:
|
||||
"""
|
||||
Gets all songs with a given title.
|
||||
"""
|
||||
|
||||
tracks = [track.title for track in self.tracks]
|
||||
results = process.extract(
|
||||
self.query,
|
||||
tracks,
|
||||
scorer=fuzz.WRatio,
|
||||
score_cutoff=Cutoff.tracks,
|
||||
limit=Limit.tracks,
|
||||
)
|
||||
|
||||
return [self.tracks[i[2]] for i in results]
|
||||
|
||||
|
||||
class SearchArtists:
|
||||
def __init__(self, artists: set[str], query: str) -> None:
|
||||
self.query = query
|
||||
self.artists = artists
|
||||
|
||||
def __call__(self) -> list:
|
||||
"""
|
||||
Gets all artists with a given name.
|
||||
"""
|
||||
|
||||
results = process.extract(
|
||||
self.query,
|
||||
self.artists,
|
||||
scorer=fuzz.WRatio,
|
||||
score_cutoff=Cutoff.artists,
|
||||
limit=Limit.artists,
|
||||
)
|
||||
|
||||
artists = [a[0] for a in results]
|
||||
return helpers.get_normalized_artists(artists)
|
||||
|
||||
|
||||
class SearchAlbums:
|
||||
def __init__(self, albums: List[models.Album], query: str) -> None:
|
||||
self.query = query
|
||||
self.albums = albums
|
||||
|
||||
def __call__(self) -> List[models.Album]:
|
||||
"""
|
||||
Gets all albums with a given title.
|
||||
"""
|
||||
|
||||
albums = [a.title.lower() for a in self.albums]
|
||||
|
||||
results = process.extract(
|
||||
self.query,
|
||||
albums,
|
||||
scorer=fuzz.WRatio,
|
||||
score_cutoff=Cutoff.albums,
|
||||
limit=Limit.albums,
|
||||
)
|
||||
|
||||
return [self.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 SearchPlaylists:
|
||||
def __init__(self, playlists: List[models.Playlist], query: str) -> None:
|
||||
self.playlists = playlists
|
||||
self.query = query
|
||||
|
||||
def __call__(self) -> List[models.Playlist]:
|
||||
playlists = [p.name for p in self.playlists]
|
||||
results = process.extract(
|
||||
self.query,
|
||||
playlists,
|
||||
scorer=fuzz.WRatio,
|
||||
score_cutoff=Cutoff.playlists,
|
||||
limit=Limit.playlists,
|
||||
)
|
||||
|
||||
return [self.playlists[i[2]] for i in results]
|
||||
@@ -1,194 +0,0 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
import mutagen
|
||||
from app import settings
|
||||
from mutagen.flac import FLAC, MutagenError
|
||||
from mutagen.id3 import ID3
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def parse_album_art(filepath: str):
|
||||
"""
|
||||
Returns the album art for a given audio file.
|
||||
"""
|
||||
|
||||
if filepath.endswith(".flac"):
|
||||
try:
|
||||
audio = FLAC(filepath)
|
||||
return audio.pictures[0].data
|
||||
except:
|
||||
return None
|
||||
elif filepath.endswith(".mp3"):
|
||||
try:
|
||||
audio = ID3(filepath)
|
||||
return audio.getall("APIC")[0].data
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def extract_thumb(filepath: str, webp_path: str) -> bool:
|
||||
"""
|
||||
Extracts the thumbnail from an audio file. Returns the path to the thumbnail.
|
||||
"""
|
||||
img_path = os.path.join(settings.THUMBS_PATH, webp_path)
|
||||
tsize = settings.THUMB_SIZE
|
||||
|
||||
if os.path.exists(img_path):
|
||||
img_size = os.path.getsize(filepath)
|
||||
|
||||
if img_size > 0:
|
||||
return True
|
||||
|
||||
album_art = parse_album_art(filepath)
|
||||
|
||||
if album_art is not None:
|
||||
img = Image.open(BytesIO(album_art))
|
||||
|
||||
try:
|
||||
small_img = img.resize((tsize, tsize), Image.ANTIALIAS)
|
||||
small_img.save(img_path, format="webp")
|
||||
except OSError:
|
||||
try:
|
||||
png = img.convert("RGB")
|
||||
small_img = png.resize((tsize, tsize), Image.ANTIALIAS)
|
||||
small_img.save(webp_path, format="webp")
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def parse_artist_tag(tags):
|
||||
"""
|
||||
Parses the artist tag from an audio file.
|
||||
"""
|
||||
try:
|
||||
artists = tags["artist"][0]
|
||||
except (KeyError, IndexError):
|
||||
artists = "Unknown"
|
||||
|
||||
return artists
|
||||
|
||||
|
||||
def parse_title_tag(tags, full_path: str):
|
||||
"""
|
||||
Parses the title tag from an audio file.
|
||||
"""
|
||||
try:
|
||||
title = tags["title"][0]
|
||||
except (KeyError, IndexError):
|
||||
title = full_path.split("/")[-1]
|
||||
|
||||
return title
|
||||
|
||||
|
||||
def parse_album_artist_tag(tags):
|
||||
"""
|
||||
Parses the album artist tag from an audio file.
|
||||
"""
|
||||
try:
|
||||
albumartist = tags["albumartist"][0]
|
||||
except (KeyError, IndexError):
|
||||
albumartist = "Unknown"
|
||||
|
||||
return albumartist
|
||||
|
||||
|
||||
def parse_album_tag(tags, full_path: str):
|
||||
"""
|
||||
Parses the album tag from an audio file.
|
||||
"""
|
||||
try:
|
||||
album = tags["album"][0]
|
||||
except (KeyError, IndexError):
|
||||
album = full_path.split("/")[-1]
|
||||
|
||||
return album
|
||||
|
||||
|
||||
def parse_genre_tag(tags):
|
||||
"""
|
||||
Parses the genre tag from an audio file.
|
||||
"""
|
||||
try:
|
||||
genre = tags["genre"][0]
|
||||
except (KeyError, IndexError):
|
||||
genre = "Unknown"
|
||||
|
||||
return genre
|
||||
|
||||
|
||||
def parse_date_tag(tags):
|
||||
"""
|
||||
Parses the date tag from an audio file.
|
||||
"""
|
||||
try:
|
||||
date = tags["date"][0]
|
||||
except (KeyError, IndexError):
|
||||
date = "Unknown"
|
||||
|
||||
return date
|
||||
|
||||
|
||||
def parse_track_number(tags):
|
||||
"""
|
||||
Parses the track number from an audio file.
|
||||
"""
|
||||
try:
|
||||
track_number = int(tags["tracknumber"][0])
|
||||
except (KeyError, IndexError, ValueError):
|
||||
track_number = 1
|
||||
|
||||
return track_number
|
||||
|
||||
|
||||
def parse_disc_number(tags):
|
||||
"""
|
||||
Parses the disc number from an audio file.
|
||||
"""
|
||||
try:
|
||||
disc_number = int(tags["discnumber"][0])
|
||||
except (KeyError, IndexError, ValueError):
|
||||
disc_number = 1
|
||||
|
||||
return disc_number
|
||||
|
||||
|
||||
def parse_copyright(tags):
|
||||
try:
|
||||
copyright = str(tags["copyright"][0])
|
||||
except (KeyError, IndexError, ValueError):
|
||||
copyright = None
|
||||
|
||||
return copyright
|
||||
|
||||
|
||||
def get_tags(fullpath: str) -> dict | None:
|
||||
"""
|
||||
Returns a dictionary of tags for a given file.
|
||||
"""
|
||||
try:
|
||||
tags = mutagen.File(fullpath, easy=True)
|
||||
except MutagenError:
|
||||
return None
|
||||
|
||||
tags = {
|
||||
"artists": parse_artist_tag(tags),
|
||||
"title": parse_title_tag(tags, fullpath),
|
||||
"albumartist": parse_album_artist_tag(tags),
|
||||
"album": parse_album_tag(tags, fullpath),
|
||||
"genre": parse_genre_tag(tags),
|
||||
"date": parse_date_tag(tags)[:4],
|
||||
"tracknumber": parse_track_number(tags),
|
||||
"discnumber": parse_disc_number(tags),
|
||||
"copyright": parse_copyright(tags),
|
||||
"length": round(tags.info.length),
|
||||
"bitrate": round(int(tags.info.bitrate) / 1000),
|
||||
"filepath": fullpath,
|
||||
"folder": os.path.dirname(fullpath),
|
||||
}
|
||||
|
||||
return tags
|
||||
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
This library contains all the functions related to tracks.
|
||||
"""
|
||||
import os
|
||||
|
||||
from app import instances
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def validate_tracks() -> None:
|
||||
"""
|
||||
Gets all songs under the ~/ directory.
|
||||
"""
|
||||
entries = instances.tracks_instance.get_all_tracks()
|
||||
|
||||
for track in tqdm(entries, desc="Validating tracks"):
|
||||
try:
|
||||
os.chmod(track["filepath"], 0o755)
|
||||
except FileNotFoundError:
|
||||
instances.tracks_instance.remove_song_by_id(track["_id"]["$oid"])
|
||||
|
||||
|
||||
def get_p_track(ptrack):
|
||||
return instances.tracks_instance.find_track_by_title_artists_album(
|
||||
ptrack["title"], ptrack["artists"], ptrack["album"])
|
||||
@@ -1,137 +0,0 @@
|
||||
"""
|
||||
This library contains the classes and functions related to the watchdog file watcher.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
from app import instances
|
||||
from app.helpers import create_hash
|
||||
from app.lib.taglib import get_tags
|
||||
from app.logger import get_logger
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
class OnMyWatch:
|
||||
"""
|
||||
Contains the methods for initializing and starting watchdog.
|
||||
"""
|
||||
|
||||
home_dir = os.path.expanduser("~")
|
||||
dirs = [home_dir]
|
||||
observers: List[Observer] = []
|
||||
|
||||
def __init__(self):
|
||||
self.observer = Observer()
|
||||
|
||||
def run(self):
|
||||
event_handler = Handler()
|
||||
|
||||
for dir in self.dirs:
|
||||
print("something")
|
||||
self.observer.schedule(event_handler, os.path.realpath(dir), recursive=True)
|
||||
self.observers.append(self.observer)
|
||||
|
||||
try:
|
||||
self.observer.start()
|
||||
print("something something")
|
||||
except OSError:
|
||||
log.error("Could not start watchdog.")
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
for o in self.observers:
|
||||
o.unschedule_all()
|
||||
o.stop()
|
||||
print("Observer Stopped")
|
||||
|
||||
for o in self.observers:
|
||||
o.join()
|
||||
|
||||
|
||||
def add_track(filepath: str) -> None:
|
||||
"""
|
||||
Processes the audio tags for a given file ands add them to the music dict.
|
||||
|
||||
Then creates a folder object for the added track and adds it to api.FOLDERS
|
||||
"""
|
||||
tags = get_tags(filepath)
|
||||
|
||||
if tags is not None:
|
||||
hash = create_hash(tags["album"], tags["albumartist"])
|
||||
tags["albumhash"] = hash
|
||||
instances.tracks_instance.insert_song(tags)
|
||||
|
||||
|
||||
def remove_track(filepath: str) -> None:
|
||||
"""
|
||||
Removes a track from the music dict.
|
||||
"""
|
||||
|
||||
instances.tracks_instance.remove_song_by_filepath(filepath)
|
||||
|
||||
|
||||
class Handler(PatternMatchingEventHandler):
|
||||
files_to_process = []
|
||||
|
||||
def __init__(self):
|
||||
print("💠 started watchdog 💠")
|
||||
PatternMatchingEventHandler.__init__(
|
||||
self,
|
||||
patterns=["*.flac", "*.mp3"],
|
||||
ignore_directories=True,
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
def on_created(self, event):
|
||||
"""
|
||||
Fired when a supported file is created.
|
||||
"""
|
||||
print("💠 created file 💠")
|
||||
self.files_to_process.append(event.src_path)
|
||||
|
||||
def on_deleted(self, event):
|
||||
"""
|
||||
Fired when a delete event occurs on a supported file.
|
||||
"""
|
||||
|
||||
remove_track(event.src_path)
|
||||
|
||||
def on_moved(self, event):
|
||||
"""
|
||||
Fired when a move event occurs on a supported file.
|
||||
"""
|
||||
tr = "share/Trash"
|
||||
|
||||
if tr in event.dest_path:
|
||||
print("trash ++")
|
||||
remove_track(event.src_path)
|
||||
|
||||
elif tr in event.src_path:
|
||||
add_track(event.dest_path)
|
||||
|
||||
elif tr not in event.dest_path and tr not in event.src_path:
|
||||
add_track(event.dest_path)
|
||||
remove_track(event.src_path)
|
||||
|
||||
def on_closed(self, event):
|
||||
"""
|
||||
Fired when a created file is closed.
|
||||
"""
|
||||
try:
|
||||
self.files_to_process.remove(event.src_path)
|
||||
add_track(event.src_path)
|
||||
except ValueError:
|
||||
"""
|
||||
The file was already removed from the list, or it was not in the list to begin with.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
watch = OnMyWatch()
|
||||
@@ -1,48 +0,0 @@
|
||||
import logging
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
|
||||
grey = "\x1b[38;20m"
|
||||
yellow = "\x1b[33;20m"
|
||||
red = "\x1b[31;20m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
reset = "\x1b[0m"
|
||||
# format = (
|
||||
# "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
|
||||
# )
|
||||
format = "[%(asctime)s] [%(levelname)s] [@%(name)s]ℹ️ %(message)s"
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: grey + format + reset,
|
||||
logging.INFO: grey + format + reset,
|
||||
logging.WARNING: yellow + format + reset,
|
||||
logging.ERROR: red + format + reset,
|
||||
logging.CRITICAL: bold_red + format + reset,
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt, "%H:%M:%S")
|
||||
return formatter.format(record)
|
||||
|
||||
|
||||
logg = logging.getLogger("ALICE_MUSIC_SERVER")
|
||||
logg.setLevel(logging.DEBUG)
|
||||
|
||||
# create console handler with a higher log level
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
|
||||
ch.setFormatter(CustomFormatter())
|
||||
|
||||
logg.addHandler(ch)
|
||||
|
||||
|
||||
def get_logger():
|
||||
return logg
|
||||
|
||||
|
||||
logg = get_logger()
|
||||
|
||||
# copied from: https://stackoverflow.com/a/56944256:
|
||||
@@ -1,194 +0,0 @@
|
||||
"""
|
||||
Contains all the models for objects generation and typing.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from operator import itemgetter
|
||||
from typing import List
|
||||
|
||||
from app import helpers
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Track:
|
||||
"""
|
||||
Track class
|
||||
"""
|
||||
|
||||
trackid: str
|
||||
title: str
|
||||
artists: list[str]
|
||||
albumartist: str
|
||||
album: str
|
||||
folder: str
|
||||
filepath: str
|
||||
length: int
|
||||
genre: str
|
||||
bitrate: int
|
||||
tracknumber: int
|
||||
discnumber: int
|
||||
albumhash: str
|
||||
date: str
|
||||
image: str
|
||||
uniq_hash: str
|
||||
copyright: str
|
||||
|
||||
def __init__(self, tags):
|
||||
(
|
||||
self.title,
|
||||
self.album,
|
||||
self.albumartist,
|
||||
self.genre,
|
||||
self.albumhash,
|
||||
self.date,
|
||||
self.folder,
|
||||
self.filepath,
|
||||
self.copyright,
|
||||
) = itemgetter(
|
||||
"title",
|
||||
"album",
|
||||
"albumartist",
|
||||
"genre",
|
||||
"albumhash",
|
||||
"date",
|
||||
"folder",
|
||||
"filepath",
|
||||
"copyright",
|
||||
)(
|
||||
tags
|
||||
)
|
||||
self.trackid = tags["_id"]["$oid"]
|
||||
self.artists = tags["artists"].split(", ")
|
||||
self.bitrate = int(tags["bitrate"])
|
||||
self.length = int(tags["length"])
|
||||
self.discnumber = int(tags["discnumber"])
|
||||
self.image = tags["albumhash"] + ".webp"
|
||||
self.tracknumber = int(tags["tracknumber"])
|
||||
|
||||
self.uniq_hash = helpers.create_hash(
|
||||
"".join(self.artists), self.album, self.title
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_unique_hash(*args):
|
||||
string = "".join(str(a) for a in args).replace(" ", "")
|
||||
return "".join([i for i in string if i.isalnum()]).lower()
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Artist:
|
||||
"""
|
||||
Artist class
|
||||
"""
|
||||
|
||||
name: str
|
||||
image: str
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.image = helpers.create_safe_name(name) + ".webp"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Album:
|
||||
"""
|
||||
Creates an album object
|
||||
"""
|
||||
|
||||
title: str
|
||||
artist: str
|
||||
hash: str
|
||||
date: int
|
||||
image: str
|
||||
count: int = 0
|
||||
duration: int = 0
|
||||
copyright: str = field(default="")
|
||||
is_soundtrack: bool = False
|
||||
is_compilation: bool = False
|
||||
is_single: bool = False
|
||||
colors: List[str] = field(default_factory=list)
|
||||
|
||||
def __init__(self, tags):
|
||||
(
|
||||
self.title,
|
||||
self.artist,
|
||||
self.date,
|
||||
self.image,
|
||||
self.hash,
|
||||
self.copyright,
|
||||
) = itemgetter("title", "artist", "date", "image", "hash", "copyright")(tags)
|
||||
|
||||
try:
|
||||
self.colors = tags["colors"]
|
||||
except KeyError:
|
||||
self.colors = []
|
||||
|
||||
@property
|
||||
def is_soundtrack(self) -> bool:
|
||||
keywords = ["motion picture", "soundtrack"]
|
||||
for keyword in keywords:
|
||||
if keyword in self.title.lower():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_compilation(self) -> bool:
|
||||
return self.artist.lower() == "various artists"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Playlist:
|
||||
"""Creates playlist objects"""
|
||||
|
||||
playlistid: str
|
||||
name: str
|
||||
tracks: List[Track]
|
||||
pretracks: list = field(init=False, repr=False)
|
||||
lastUpdated: int
|
||||
image: str
|
||||
thumb: str
|
||||
description: str = ""
|
||||
count: int = 0
|
||||
"""A list of track objects in the playlist"""
|
||||
|
||||
def __init__(self, data):
|
||||
self.playlistid = data["_id"]["$oid"]
|
||||
self.name = data["name"]
|
||||
self.description = data["description"]
|
||||
self.image = self.create_img_link(data["image"])
|
||||
self.thumb = self.create_img_link(data["thumb"])
|
||||
self.pretracks = data["pre_tracks"]
|
||||
self.tracks = []
|
||||
self.lastUpdated = data["lastUpdated"]
|
||||
self.count = len(self.pretracks)
|
||||
|
||||
def create_img_link(self, image: str):
|
||||
if image:
|
||||
return image
|
||||
|
||||
return "default.webp"
|
||||
|
||||
def update_playlist(self, data: dict):
|
||||
self.name = data["name"]
|
||||
self.description = data["description"]
|
||||
self.lastUpdated = data["lastUpdated"]
|
||||
|
||||
if data["image"]:
|
||||
self.image = self.create_img_link(data["image"])
|
||||
self.thumb = self.create_img_link(data["thumb"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class Folder:
|
||||
name: str
|
||||
path: str
|
||||
trackcount: int
|
||||
is_sym: bool = False
|
||||
"""The number of tracks in the folder"""
|
||||
|
||||
def __init__(self, data) -> None:
|
||||
self.name = data["name"]
|
||||
self.path = data["path"]
|
||||
self.is_sym = data["is_sym"]
|
||||
self.trackcount = data["trackcount"]
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
This module contains patch functions to modify existing data in the database.
|
||||
"""
|
||||
@@ -1,60 +0,0 @@
|
||||
"""
|
||||
Contains the functions to prepare the server for use.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from app import settings
|
||||
|
||||
|
||||
class CopyFiles:
|
||||
"""Copies assets to the app directory."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
files = [{
|
||||
"src": "assets",
|
||||
"dest": os.path.join(settings.APP_DIR, "assets"),
|
||||
"is_dir": True,
|
||||
}]
|
||||
|
||||
for entry in files:
|
||||
src = os.path.join(os.getcwd(), entry["src"])
|
||||
|
||||
if entry["is_dir"]:
|
||||
shutil.copytree(
|
||||
src,
|
||||
entry["dest"],
|
||||
ignore=shutil.ignore_patterns("*.pyc", ),
|
||||
copy_function=shutil.copy2,
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
break
|
||||
|
||||
shutil.copy2(src, entry["dest"])
|
||||
|
||||
|
||||
def create_config_dir() -> None:
|
||||
"""
|
||||
Creates the config directory if it doesn't exist.
|
||||
"""
|
||||
|
||||
_home_dir = os.path.expanduser("~")
|
||||
config_folder = os.path.join(_home_dir, settings.CONFIG_FOLDER)
|
||||
|
||||
dirs = [
|
||||
"",
|
||||
"images",
|
||||
os.path.join("images", "artists"),
|
||||
os.path.join("images", "thumbnails"),
|
||||
os.path.join("images", "playlists"),
|
||||
]
|
||||
|
||||
for _dir in dirs:
|
||||
path = os.path.join(config_folder, _dir)
|
||||
exists = os.path.exists(path)
|
||||
|
||||
if not exists:
|
||||
os.makedirs(path)
|
||||
os.chmod(path, 0o755)
|
||||
|
||||
CopyFiles()
|
||||
@@ -1,80 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from app import models
|
||||
|
||||
|
||||
def date_string_to_time_passed(prev_date: str) -> str:
|
||||
"""
|
||||
Converts a date string to time passed. eg. 2 minutes ago, 1 hour ago, yesterday, 2 days ago, 2 weeks ago, etc.
|
||||
"""
|
||||
|
||||
now = datetime.now()
|
||||
then = datetime.strptime(prev_date, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
diff = now - then
|
||||
days = diff.days
|
||||
|
||||
if days < 0:
|
||||
return "in the future"
|
||||
|
||||
elif days == 0:
|
||||
seconds = diff.seconds
|
||||
if seconds < 15:
|
||||
return "now"
|
||||
elif seconds < 60:
|
||||
return str(seconds) + " seconds ago"
|
||||
elif seconds < 3600:
|
||||
return str(seconds // 60) + " minutes ago"
|
||||
else:
|
||||
return str(seconds // 3600) + " hours ago"
|
||||
|
||||
elif days == 1:
|
||||
return "yesterday"
|
||||
elif days < 7:
|
||||
return str(days) + " days ago"
|
||||
elif days < 30:
|
||||
if days < 14:
|
||||
return "1 week ago"
|
||||
|
||||
return str(days // 7) + " weeks ago"
|
||||
elif days < 365:
|
||||
if days < 60:
|
||||
return "1 month ago"
|
||||
|
||||
return str(days // 30) + " months ago"
|
||||
elif days > 365:
|
||||
if days < 730:
|
||||
return "1 year ago"
|
||||
|
||||
return str(days // 365) + " years ago"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Playlist:
|
||||
playlistid: str
|
||||
name: str
|
||||
image: str
|
||||
thumb: str
|
||||
lastUpdated: int
|
||||
description: str
|
||||
count: int = 0
|
||||
duration: int = 0
|
||||
|
||||
def __init__(self,
|
||||
p: models.Playlist,
|
||||
construct_last_updated: bool = True) -> None:
|
||||
self.playlistid = p.playlistid
|
||||
self.name = p.name
|
||||
self.image = p.image
|
||||
self.thumb = p.thumb
|
||||
self.lastUpdated = p.lastUpdated
|
||||
self.description = p.description
|
||||
self.count = p.count
|
||||
|
||||
if construct_last_updated:
|
||||
self.lastUpdated = self.get_l_updated(p.lastUpdated)
|
||||
|
||||
@staticmethod
|
||||
def get_l_updated(date: str) -> str:
|
||||
return date_string_to_time_passed(date)
|
||||
@@ -1,35 +0,0 @@
|
||||
"""
|
||||
Contains default configs
|
||||
"""
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
# paths
|
||||
CONFIG_FOLDER = ".alice"
|
||||
HOME_DIR = os.path.expanduser("~")
|
||||
APP_DIR = os.path.join(HOME_DIR, CONFIG_FOLDER)
|
||||
IMG_PATH = os.path.join(APP_DIR, "images")
|
||||
|
||||
THUMBS_PATH = os.path.join(IMG_PATH, "thumbnails")
|
||||
TEST_DIR = "/home/cwilvx/Music/Link to Music/Chill/Wolftyla Radio"
|
||||
# HOME_DIR = TEST_DIR
|
||||
|
||||
# URLS
|
||||
IMG_BASE_URI = "http://127.0.0.1:8900/images/"
|
||||
IMG_ARTIST_URI = IMG_BASE_URI + "artists/"
|
||||
IMG_THUMB_URI = IMG_BASE_URI + "thumbnails/"
|
||||
IMG_PLAYLIST_URI = IMG_BASE_URI + "playlists/"
|
||||
|
||||
# defaults
|
||||
DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp"
|
||||
|
||||
LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a"
|
||||
|
||||
CPU_COUNT = multiprocessing.cpu_count()
|
||||
|
||||
THUMB_SIZE: int = 400
|
||||
"""
|
||||
The size of extracted in pixels
|
||||
"""
|
||||
|
||||
LOGGER_ENABLE: bool = True
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +0,0 @@
|
||||
from app import create_app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True, threaded=True, host="0.0.0.0", port=9876, use_reloader=False)
|
||||
Generated
-742
@@ -1,742 +0,0 @@
|
||||
[[package]]
|
||||
name = "cachelib"
|
||||
version = "0.7.0"
|
||||
description = "A collection of cache libraries in the same API interface."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2022.5.18.1"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.0.12"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "colorgram.py"
|
||||
version = "1.2.0"
|
||||
description = "A Python module for extracting colors from images. Get a palette of any picture!"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pillow = ">=3.3.1"
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "2.1.2"
|
||||
description = "A simple framework for building complex web applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0"
|
||||
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
|
||||
itsdangerous = ">=2.0"
|
||||
Jinja2 = ">=3.0"
|
||||
Werkzeug = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-caching"
|
||||
version = "1.11.1"
|
||||
description = "Adds caching support to Flask applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
cachelib = "*"
|
||||
Flask = "*"
|
||||
|
||||
[[package]]
|
||||
name = "flask-cors"
|
||||
version = "3.0.10"
|
||||
description = "A Flask extension adding a decorator for CORS support"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=0.9"
|
||||
Six = "*"
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "20.1.0"
|
||||
description = "WSGI HTTP Server for UNIX"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
eventlet = ["eventlet (>=0.24.1)"]
|
||||
gevent = ["gevent (>=1.4.0)"]
|
||||
setproctitle = ["setproctitle"]
|
||||
tornado = ["tornado (>=0.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.3"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "4.11.4"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
|
||||
perf = ["ipython"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.1.2"
|
||||
description = "Safely pass data to untrusted environments and back."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[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.1.2"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.1"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "mutagen"
|
||||
version = "1.45.1"
|
||||
description = "read and write audio tags for many formats"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5, <4"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "9.1.1"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"]
|
||||
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||
|
||||
[[package]]
|
||||
name = "progress"
|
||||
version = "1.6"
|
||||
description = "Easy to use progress bars"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pymongo"
|
||||
version = "4.1.1"
|
||||
description = "Python driver for MongoDB <http://www.mongodb.org>"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
|
||||
[package.extras]
|
||||
aws = ["pymongo-auth-aws (<2.0.0)"]
|
||||
encryption = ["pymongocrypt (>=1.2.0,<2.0.0)"]
|
||||
gssapi = ["pykerberos"]
|
||||
ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)", "certifi"]
|
||||
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"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.64.0"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["py-make (>=0.1.0)", "twine", "wheel"]
|
||||
notebook = ["ipywidgets (>=6)"]
|
||||
slack = ["slack-sdk"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.9"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "2.1.8"
|
||||
description = "Filesystem events monitoring"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "2.1.2"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.8.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "6247b9ff39d46d078e0f97423034ff5e26542ad74fece6a54547b72877474f11"
|
||||
|
||||
[metadata.files]
|
||||
cachelib = [
|
||||
{file = "cachelib-0.7.0-py3-none-any.whl", hash = "sha256:80fa73dda398672329dab6c8e9e9bad03fd36dc4da40d911d7de308c91e8481e"},
|
||||
{file = "cachelib-0.7.0.tar.gz", hash = "sha256:df254f3b900dc8684d8ebdd146c731ddb45edc6233a6cf7e3e834c949f360726"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"},
|
||||
{file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
||||
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
"colorgram.py" = [
|
||||
{file = "colorgram.py-1.2.0-py2.py3-none-any.whl", hash = "sha256:e990769fa6df7261a450c7d5bef3a1a062f09ba1214bff67b4d6f02970a1a27b"},
|
||||
{file = "colorgram.py-1.2.0.tar.gz", hash = "sha256:e77766a5f9de7207bdef8f1c22a702cbf09630eae3bc46a450b9d9f12a7bfdbf"},
|
||||
]
|
||||
flask = [
|
||||
{file = "Flask-2.1.2-py3-none-any.whl", hash = "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"},
|
||||
{file = "Flask-2.1.2.tar.gz", hash = "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477"},
|
||||
]
|
||||
flask-caching = [
|
||||
{file = "Flask-Caching-1.11.1.tar.gz", hash = "sha256:28af189e97defb9e39b43ebe197b54a58aaee81bdeb759f46d969c26d7aa7810"},
|
||||
{file = "Flask_Caching-1.11.1-py3-none-any.whl", hash = "sha256:36592812eec6cba86eca48bcda74eff24bfd6c8eaf6056ca0184474bb78c0dc4"},
|
||||
]
|
||||
flask-cors = [
|
||||
{file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"},
|
||||
{file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"},
|
||||
]
|
||||
gunicorn = [
|
||||
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
|
||||
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"},
|
||||
{file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"},
|
||||
]
|
||||
itsdangerous = [
|
||||
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
||||
]
|
||||
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.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
|
||||
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
|
||||
]
|
||||
mutagen = [
|
||||
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
||||
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"},
|
||||
{file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"},
|
||||
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"},
|
||||
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"},
|
||||
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"},
|
||||
{file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"},
|
||||
{file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"},
|
||||
{file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"},
|
||||
{file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"},
|
||||
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"},
|
||||
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"},
|
||||
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"},
|
||||
{file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"},
|
||||
{file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"},
|
||||
{file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"},
|
||||
{file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"},
|
||||
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"},
|
||||
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"},
|
||||
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"},
|
||||
{file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"},
|
||||
{file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"},
|
||||
{file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"},
|
||||
{file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"},
|
||||
{file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"},
|
||||
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"},
|
||||
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"},
|
||||
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"},
|
||||
{file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"},
|
||||
{file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"},
|
||||
{file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"},
|
||||
{file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"},
|
||||
{file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"},
|
||||
{file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"},
|
||||
{file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"},
|
||||
{file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"},
|
||||
{file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"},
|
||||
{file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"},
|
||||
{file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"},
|
||||
]
|
||||
progress = [
|
||||
{file = "progress-1.6.tar.gz", hash = "sha256:c9c86e98b5c03fa1fe11e3b67c1feda4788b8d0fe7336c2ff7d5644ccfba34cd"},
|
||||
]
|
||||
pymongo = [
|
||||
{file = "pymongo-4.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eff9818b7671a55f1ce781398607e0d8c304cd430c0581fbe15b868a7a371c27"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:7507439cd799295893b5602f438f8b6a0f483efb00720df1aa33a39102b41bcf"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:c759e1e0333664831d8d1d6b26cf59f23f3707758f696c71f506504b33130f81"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:69beffb048de19f7c18617b90e38cbddfac20077b1826c27c3fe2e3ef8ac5a43"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:cbcac9263f500da94405cc9fc7e7a42a3ba6c2fe88b2cd7039737cba44c66889"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:d4ba5b4f1a0334dbe673f767f28775744e793fcb9ea57a1d72bc622c9f90e6b4"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:c575f9499e5f540e034ff87bef894f031ae613a98b0d1d3afcc1f482527d5f1c"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89f32d8450e15b0c11efdc81e2704d68c502c889d48415a50add9fa031144f75"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1417cb339a367a5dfd0e50193a1c0e87e31325547a0e7624ee4ff414c0b53b3"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:56b856a459762a3c052987e28ed2bd4b874f0be6671d2cc4f74c4891f47f997a"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a938d4d5b530f8ea988afb80817209eabc150c53b8c7af79d40080313a35e470"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c604831daf2e7e5979ecd97a90cb8c4a7bae208ff45bc792e32eae09c3281afb"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-win32.whl", hash = "sha256:f9405c02af86850e0a8a8ba777b7e7609e0d07bff46adc4f78892cc2d5456018"},
|
||||
{file = "pymongo-4.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:e13ddfe2ead9540e8773cae098f54c5206d6fcef64846a3e5042db47fc3a41ed"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:7f36eacc70849d40ce86c85042ecfcbeab810691b1a3b08062ede32a2d6521ac"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:302ac0f4825501ab0900b8f1a2bb2dc7d28f69c7f15fbc799fb26f9b9ebb1ecb"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9ee1b019a4640bf39c0705ab65e934cfe6b89f1a8dc26f389fae3d7c62358d6f"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c3637cfce519560e2a2579d05eb81e912d109283b8ddc8de46f57ec20d273d92"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:a0d7c6d6fbca62508ea525abd869fca78ecf68cd3bcf6ae67ec478aa37cf39c0"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:f1fba193ab2f25849e24caa4570611aa2f80bc1c1ba791851523734b4ed69e43"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:c8a2743dd50629c0222f26c5f55975e45841d985b4b1c7a54b3f03b53de3427d"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:8357aa727094798f1d831339ecfd8b3e388c01db6015a3cbd51790cb75e39994"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f7e3872fb7b61ec574b7e04302ea03928b670df583f8691cb1df6e54cd42b19"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4aa4800530782f7d38aeb169476a5bc692aacc394686f0ca3866e4bb85c9aa3f"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d69a3d980ecbf7238ab37b9027c87ad3b278bb3742a150fc33b5a8a9d990431"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df9084e06efb3d59608a6a443faa9861828585579f0ae8e95f5a4dab70f1a00f"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be3ba736aabf856195199208ed37459408c932940cbccd2dc9f6ff2e800b0261"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f106468062ac7ff03e3522a66cb7b36c662326d8eb7af1be0f30563740ff002"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:019a4c13ef1d9accd08de70247068671b116a0383adcd684f6365219f29f41cd"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-win32.whl", hash = "sha256:a7d1c8830a7bc10420ceb60a256d25ab5b032a6dad12a46af6ab2e470cee9124"},
|
||||
{file = "pymongo-4.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:08a619c92769bd7346434dfc331a3aa8dc63bee80ed0be250bb0e878c69a6f3e"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:c1349331fa743eed4042f9652200e60596f8beb957554acbcbb42aad4272c606"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8a1de8931cdad8cd12724e12a6167eef8cb478cc3ee5d2c9f4670c934f2975e1"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:86b18420f00d5977bda477369ac85e04185ef94046a04ae0d85f5a807d1a8eb4"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:84dc6bfeaeba98fe93fc837b12f9af4842694cdbde18083f150e80aec3de88f9"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:306336dab4537b2343e52ec34017c3051c3aee5a961fff4915ab27f7e6d9b1e9"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:c481cd1af2a77f58f495f7f87c2d715c6f1179d07c1ec927cca1f7977a2d99aa"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:cce1b7a680653e31ff2b252f19a39f1ded578a35a96c419ddb9632c62d2af7d8"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:35d02603c2318676fca5049cdc722bb2e7a378eaccf139ad767365e0eb3bcdbe"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf96799b3e5e2e2f6dbca015f72b28e7ae415ce8147472f89a3704a035d6336d"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7aa40509dd9f75c256f0a7533d5e2ccef711dbbf0d91c13ac937d21d76d71656"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d1cdece06156542c18b691511a01fe78a694b9fa287ffd8e15680dbf2beeed5"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17df40753085ccba38a0e150001f757910d66440d9b5deced30ed4cc8b45b6f3"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4516a5ce2beaebddc74d6e304ed520324dda99573c310ef4078284b026f81e93"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52c8b7bffd2140818ade2aa28c24cfe47935a7273a3bb976d1d8fb17e716536f"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dae2cf84a09329617b08731b95ad1fc98d50a9b40c2007e351438bd119a2f7a"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-win32.whl", hash = "sha256:0a3474e6a0df0077a44573727341df6627042df5ca61ea5373c157bb6512ccc7"},
|
||||
{file = "pymongo-4.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:deb83cc9f639045e2febcc8d4306d4b83893af8d895f2ed70aa342a3430b534c"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:298908478d07871dbe17e9ccd37a10a27ad3f37cc1faaf0cc4d205da3c3e8539"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5d6ef3fa41f3e3be93483a77f81dea8c7ce5ed4411382a31af2b09b9ec5d9585"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d06ed18917dbc7a938c4231cbbec52a7e474be270b2ef9208abb4d5a34f5ceb9"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:4e4d2babb8737d650250d0fa940ffa1b88aa92b8eb399af093734950a1eeca45"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:303d1b3da2461586379d98b344b529598c8156857285ba5bd156dab1c875d1f6"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:6396f0db060db9d8751167ea08f3a77a41a71cd39236fade4409394e57b377e8"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:9a4ea87a0401c06b687db29e2ae836b2b58480ab118cb6eea8ac2ef45a4345f8"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:49bb36986f11da2da190a2e777a411c0a28eeb8623850091ea8099b84e3860c7"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cae9c935cdc53e4729920543b7d990615a115d85f32144773bc4b2b05144628"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:baf7546afd27be4f96f23307d7c295497fb512875167743b14a7457b95761294"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07f50a3b8a3afb086089abcd9ab562fb2a27b63fd7017ca13dfe7b663c8f3762"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8a1c766de29173ddbd316dbd75a97b19a4cf9ac45a39ad4f53426e5df1483b"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a2c377106fe01a57bad0f703653de286d56ee5285ed36c6953535cfa11f928"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dfb89e92746e4a1e0d091cba73d6cc1e16b4094ebdbb14c2e96a80320feb1ad7"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bb21e2f35d6f09aa4a6df0c716f41e036cfcf05a98323b50294f93085ad775e9"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-win32.whl", hash = "sha256:dbe92a8808cefb284e235b8f82933d7d2e24ff929fe5d53f1fd3ca55fced4b58"},
|
||||
{file = "pymongo-4.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e6eecd027b6ba5617ea6af3e12e20d578d8f4ad1bf51a9abe69c6fd4835ea532"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb4445e3721720c5ca14c0650f35c263b3430e6e16df9d2504618df914b3fb99"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f6db4f00d3baad615e99a865539391243d12b113fb628ebda1d7794ce02d5a10"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:571a3e1ef4abeb4ac719ac381f5aada664627b4ee048d9995e93b4bcd0f70601"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c03eb43d15c8af58159e7561076634d565530aaacaf48cf4e070c3501e88a372"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:30d35a8855f328a85e5002f0908b24e500efdf8f5f78b73098995ce111baa2a9"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:33a5693e8d1fbb7743b7e867d43c1095652a0c6fedddab6cefe6020bee2ca393"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a35f1937b0560587d478fd2259a6d4f66cf511c9d28e90b52b183745eaa77d95"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:4fd5c4f25d8d488ee5701c3ec786f52907dca653b47ce8709bcc2bfb0f5506ae"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db8a9cbe965c7343feab2e2bf9a3771f303f8a7ca401dececb6ef28e06b3b18c"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f55a602d55e8f0feafde533c69dfd29bf0e54645ab0996b605613cda6894a85"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4a35e83abfdac7095430e1c1476e0871e4b234e936f4a7a7631531b09a4f198"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32e785c37f6a0e844788c6085ea2c9c0c528348c22cebe91896705a92f2b1b26"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc62ba37bcb42e4146b853940b65a2de31c2962d2b6da9bc3ce28270d13b5c4e"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d86511ef8217822fb8716460aaa1ece31fe9e8a48900e541cb35acb7c35e9e2e"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4956384340eec7b526149ac126c8aa11d32441cb3ce77a690cb4821d0d0635c"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-win32.whl", hash = "sha256:3139c9ddee379c22a9109a0b3bf4cdb64597db2bbd3909f7a2825b47226977a4"},
|
||||
{file = "pymongo-4.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:f0aea377b9dfc166c8fa05bb158c30ee3d53d73f0ed2fc05ba6c638d9563422f"},
|
||||
{file = "pymongo-4.1.1.tar.gz", hash = "sha256:d7b8f25c9b0043cbaf77b8b895814e33e7a3c807a097377c07e1bd49946030d5"},
|
||||
]
|
||||
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"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
tqdm = [
|
||||
{file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"},
|
||||
{file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
|
||||
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
|
||||
]
|
||||
watchdog = [
|
||||
{file = "watchdog-2.1.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:676263bee67b165f16b05abc52acc7a94feac5b5ab2449b491f1a97638a79277"},
|
||||
{file = "watchdog-2.1.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aa68d2d9a89d686fae99d28a6edf3b18595e78f5adf4f5c18fbfda549ac0f20c"},
|
||||
{file = "watchdog-2.1.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e2e51c53666850c3ecffe9d265fc5d7351db644de17b15e9c685dd3cdcd6f97"},
|
||||
{file = "watchdog-2.1.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7721ac736170b191c50806f43357407138c6748e4eb3e69b071397f7f7aaeedd"},
|
||||
{file = "watchdog-2.1.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ce7376aed3da5fd777483fe5ebc8475a440c6d18f23998024f832134b2938e7b"},
|
||||
{file = "watchdog-2.1.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f9ee4c6bf3a1b2ed6be90a2d78f3f4bbd8105b6390c04a86eb48ed67bbfa0b0b"},
|
||||
{file = "watchdog-2.1.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:68dbe75e0fa1ba4d73ab3f8e67b21770fbed0651d32ce515cd38919a26873266"},
|
||||
{file = "watchdog-2.1.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0c520009b8cce79099237d810aaa19bc920941c268578436b62013b2f0102320"},
|
||||
{file = "watchdog-2.1.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efcc8cbc1b43902571b3dce7ef53003f5b97fe4f275fe0489565fc6e2ebe3314"},
|
||||
{file = "watchdog-2.1.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:746e4c197ec1083581bb1f64d07d1136accf03437badb5ff8fcb862565c193b2"},
|
||||
{file = "watchdog-2.1.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ae17b6be788fb8e4d8753d8d599de948f0275a232416e16436363c682c6f850"},
|
||||
{file = "watchdog-2.1.8-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ddde157dc1447d8130cb5b8df102fad845916fe4335e3d3c3f44c16565becbb7"},
|
||||
{file = "watchdog-2.1.8-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4978db33fc0934c92013ee163a9db158ec216099b69fce5aec790aba704da412"},
|
||||
{file = "watchdog-2.1.8-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b962de4d7d92ff78fb2dbc6a0cb292a679dea879a0eb5568911484d56545b153"},
|
||||
{file = "watchdog-2.1.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1e5d0fdfaa265c29dc12621913a76ae99656cf7587d03950dfeb3595e5a26102"},
|
||||
{file = "watchdog-2.1.8-py3-none-manylinux2014_armv7l.whl", hash = "sha256:036ed15f7cd656351bf4e17244447be0a09a61aaa92014332d50719fc5973bc0"},
|
||||
{file = "watchdog-2.1.8-py3-none-manylinux2014_i686.whl", hash = "sha256:2962628a8777650703e8f6f2593065884c602df7bae95759b2df267bd89b2ef5"},
|
||||
{file = "watchdog-2.1.8-py3-none-manylinux2014_ppc64.whl", hash = "sha256:156ec3a94695ea68cfb83454b98754af6e276031ba1ae7ae724dc6bf8973b92a"},
|
||||
{file = "watchdog-2.1.8-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:47598fe6713fc1fee86b1ca85c9cbe77e9b72d002d6adeab9c3b608f8a5ead10"},
|
||||
{file = "watchdog-2.1.8-py3-none-manylinux2014_s390x.whl", hash = "sha256:fed4de6e45a4f16e4046ea00917b4fe1700b97244e5d114f594b4a1b9de6bed8"},
|
||||
{file = "watchdog-2.1.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:24dedcc3ce75e150f2a1d704661f6879764461a481ba15a57dc80543de46021c"},
|
||||
{file = "watchdog-2.1.8-py3-none-win32.whl", hash = "sha256:6ddf67bc9f413791072e3afb466e46cc72c6799ba73dea18439b412e8f2e3257"},
|
||||
{file = "watchdog-2.1.8-py3-none-win_amd64.whl", hash = "sha256:88ef3e8640ef0a64b7ad7394b0f23384f58ac19dd759da7eaa9bc04b2898943f"},
|
||||
{file = "watchdog-2.1.8-py3-none-win_ia64.whl", hash = "sha256:0fb60c7d31474b21acba54079ce9ff0136411183e9a591369417cddb1d7d00d7"},
|
||||
{file = "watchdog-2.1.8.tar.gz", hash = "sha256:6d03149126864abd32715d4e9267d2754cede25a69052901399356ad3bc5ecff"},
|
||||
]
|
||||
werkzeug = [
|
||||
{file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"},
|
||||
{file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"},
|
||||
]
|
||||
zipp = [
|
||||
{file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
|
||||
{file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
[tool.poetry]
|
||||
name = "Alice Server"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["geoffrey45 <geoffreymungai45@gmail.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
Flask = "^2.0.2"
|
||||
Flask-Cors = "^3.0.10"
|
||||
mutagen = "^1.45.1"
|
||||
pymongo = "^4.0.1"
|
||||
requests = "^2.27.1"
|
||||
watchdog = "^2.1.6"
|
||||
progress = "^1.6"
|
||||
gunicorn = "^20.1.0"
|
||||
Pillow = "^9.0.1"
|
||||
Flask-Caching = "^1.11.1"
|
||||
"colorgram.py" = "^1.2.0"
|
||||
tqdm = "^4.64.0"
|
||||
rapidfuzz = "^2.0.11"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -1,39 +0,0 @@
|
||||
# Fixes !
|
||||
|
||||
- [ ] Click on artist image to go to artist page ⚠
|
||||
- [ ] Play next song if current song can't be loaded ⚠
|
||||
<!-- -->
|
||||
- [ ] Removing song duplicates from queries
|
||||
- [ ] Add support for WAV files
|
||||
- [ ] Compress thumbnails
|
||||
|
||||
# Features +
|
||||
|
||||
## Needed features
|
||||
|
||||
- [ ] Adding songs to queue
|
||||
<!-- -->
|
||||
- [ ] Add keyboard shortcuts
|
||||
- [ ] Adjust volume
|
||||
- [ ] Add listening statistics for all songs
|
||||
- [ ] Extract color from artist image [for use with artist card gradient]
|
||||
- [ ] Adding songs to favorites
|
||||
- [ ] Playing song radio
|
||||
|
||||
## Future features
|
||||
|
||||
- [ ] Toggle shuffle
|
||||
- [ ] Toggle repeat
|
||||
- [ ] Suggest similar artists
|
||||
- [ ] Getting artist info
|
||||
- [ ] Create a Python script to build, bundle and serve the app
|
||||
- [ ] Getting extra song info (probably from genius)
|
||||
- [ ] Getting lyrics
|
||||
- [ ] Sorting songs
|
||||
- [ ] Suggest undiscorvered artists, albums and songs
|
||||
- [ ] Remember last played song
|
||||
- [ ] Add next and previous song transition and progress bar reset animations
|
||||
- [ ] Add playlist to folder
|
||||
- [ ] Add functionality to 'Listen now' button
|
||||
- [ ] Paginated requests for songs
|
||||
- [ ] Package app as installable PWA
|
||||
@@ -1,11 +0,0 @@
|
||||
# commands to install and start mongodb ubuntu 20.04
|
||||
wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -
|
||||
sudo apt-get install gnupg
|
||||
wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -
|
||||
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mongodb
|
||||
|
||||
sudo systemctl start mongodb
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable mongodb
|
||||
@@ -1,22 +0,0 @@
|
||||
# ppath=$(poetry run which python)
|
||||
|
||||
# $ppath manage.py
|
||||
|
||||
#python manage.py
|
||||
|
||||
gpath=$(poetry run which gunicorn)
|
||||
while getopts ':s' opt; do
|
||||
case $opt in
|
||||
s)
|
||||
echo "Starting Alice server"
|
||||
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()"
|
||||
@@ -1,5 +0,0 @@
|
||||
from app import create_app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True, threaded=True)
|
||||
Reference in New Issue
Block a user