move server code to alice-core repo

~ remove server code from this repo
This commit is contained in:
geoffrey45
2022-08-03 16:16:03 +03:00
parent 822c264bfc
commit 6df15fe77f
45 changed files with 0 additions and 4222 deletions
-30
View File
@@ -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
-21
View File
@@ -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()
-113
View File
@@ -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}
-57
View File
@@ -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()
# }
-28
View File
@@ -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),
}
-143
View File
@@ -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}
-216
View File
@@ -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,
}
-40
View File
@@ -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")
-214
View File
@@ -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
-80
View File
@@ -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
-79
View File
@@ -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
}},
)
-38
View File
@@ -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)
-85
View File
@@ -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},
)
-30
View File
@@ -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)
-191
View File
@@ -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)
View File
-14
View File
@@ -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
-180
View File
@@ -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
-226
View File
@@ -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]
-73
View File
@@ -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)
-12
View File
@@ -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()
-3
View File
@@ -1,3 +0,0 @@
"""
This module contains all the data processing and non-API libraries
"""
-158
View File
@@ -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
-49
View File
@@ -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
-73
View File
@@ -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
-157
View File
@@ -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)
-157
View File
@@ -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)
-128
View File
@@ -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]
-194
View File
@@ -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
-25
View File
@@ -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"])
-137
View File
@@ -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()
-48
View File
@@ -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:
-194
View File
@@ -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"]
-3
View File
@@ -1,3 +0,0 @@
"""
This module contains patch functions to modify existing data in the database.
"""
-60
View File
@@ -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()
-80
View File
@@ -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)
-35
View File
@@ -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

-5
View File
@@ -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)
-742
View File
@@ -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"},
]
-27
View File
@@ -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"
-39
View File
@@ -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
-11
View File
@@ -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
-22
View File
@@ -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()"
-5
View File
@@ -1,5 +0,0 @@
from app import create_app
if __name__ == '__main__':
app = create_app()
app.run(debug=True, threaded=True)