8 Commits

Author SHA1 Message Date
8a11b9da03 navidrome analyze & generate update 2025-10-30 11:57:40 +01:00
61bc0074c0 navidrome updater 2025-10-30 02:02:15 +01:00
95a6d0d6a1 improve write file + restore ELS format 2025-10-23 10:43:45 +02:00
e761946398 date added 2025-10-23 10:43:35 +02:00
e71346e8f3 choose json style + simplificate write json
Refactoring dict construction
Don't take so much more time...
2025-10-23 10:43:35 +02:00
ce680beeb3 ruff 2025-10-22 10:09:54 +02:00
93b3632bb4 Little py script adaptation 2025-10-22 10:09:03 +02:00
7c7bc48d09 WIP Cover POC 2024-01-30 01:57:12 +01:00
15 changed files with 724 additions and 196 deletions

8
.gitignore vendored
View File

@@ -1,8 +1,8 @@
node_modules/
.vscode/
/es-albums.json
/es-artists.json
/es-songs.json
/es-albums.*
/es-artists.*
/es-songs.*
/iTunesLibrary.xml
# Working files
@@ -16,3 +16,5 @@ dashboard/resultToStudy.json
sand_box.py
rating_test.py
iTunesGraphParser.my.py
iTunes Library.xml
navidrome/navidrome.db

11
Justfile Normal file
View File

@@ -0,0 +1,11 @@
parse:
-rm -v es-*
python3 iTunesParser.py -f iTunes\ Library.xml -F json
send-all:
just send songs
just send artists
just send albums
send type:
curl -X POST 'localhost:7700/indexes/itunes-{{type}}/documents?primaryKey=Persistent%20ID' -H 'Content-Type: application/json' -H 'Authorization: Bearer aSampleMasterKey' --data-binary @es-{{type}}.json

View File

@@ -7,7 +7,9 @@
</div>
<div class="alert alert-warning">
<img class="images" src="data:image/png;base64, {{album.Cover}}" height="500"/>
<h3>Debug Zone</h3>
ID: {{album.id}}<br />
Returned song: {{songs.length}}<br />
Theorical number song: {{ album ? album['Track Count'] : "" }}
<span *ngIf="album && (songs.length == album['Track Count'])" class="glyphicon glyphicon-ok" style="color:green"></span>

View File

@@ -1,5 +1,7 @@
export class Album {
id: number;
Name: string;
Cover: string;
Album: string;
Artist: Array<string>;
Rating: number;

View File

@@ -42,17 +42,16 @@ for use in the D3.js JavaScript library.
"""
import argparse
import datetime
import hashlib
import io
import json
import os
import plistlib
import sys
import argparse
import hashlib
class SetEncoder(json.JSONEncoder):
class JsonCustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, set):
return list(obj)
@@ -64,12 +63,8 @@ class SetEncoder(json.JSONEncoder):
class ITunesParser:
"""
Parse an iTunes Library and produce JSON - for ELS
Parse an iTunes Library and produce JSON - for ELS
"""
SONG_INDEX = 'itunes-songs'
ALBUM_INDEX = 'itunes-albums'
ARTIST_INDEX = 'itunes-artists'
# TODO Put variables in a config files or in a python library
def __init__(self):
self._tracks = {}
@@ -78,268 +73,290 @@ class ITunesParser:
def _read_tracks(self, library_file):
"""
Read library file and return Tracks key of dict.
Dict may contains
- Major Version
- Minor Version
- Date
- Application Version
- Features
- Show Content Ratings
- Music Folder
- Library Persistent ID
- Tracks
- ...
Read library file and return Tracks key of dict.
Dict may contains
- Major Version
- Minor Version
- Date
- Application Version
- Features
- Show Content Ratings
- Music Folder
- Library Persistent ID
- Tracks
- ...
"""
plist = plistlib.load(open(library_file, 'rb'))
return plist['Tracks']
plist = plistlib.load(open(library_file, "rb"))
return plist["Tracks"]
def parse(self, library_file):
def parse(self, library_file) -> dict:
"""
Return an output JSON for an ELS Bulk request - Not a correct format
This method call process_album & process_artist
TODO Just return a _correct_ JSON and treat in another place/class
Return an output JSON for an ELS Bulk request - Not a correct format
This method call process_album & process_artist
TODO Just return a _correct_ JSON and treat in another place/class
"""
tracks = self._read_tracks(library_file)
for _, track in tracks.items():
# Filter out any non-music
if track['Track Type'] != 'File':
if track["Track Type"] != "File":
continue
if 'Podcast' in track or 'Has Video' in track:
if "Podcast" in track or "Has Video" in track:
continue
# Each keeped track are stored
self._tracks[track['Persistent ID']] = track
self._tracks[track["Persistent ID"]] = track
# Retrieve for each track artist information
self._process_artist(track)
# Retrieve for each track album information
self._process_album(track)
ret = {
'songs': self._tracks,
'albums': self._albums,
'artists': self._artists
}
ret = {"songs": self._tracks, "albums": self._albums, "artists": self._artists}
return ret
def _process_artist(self, track):
"""
Process artists in the track part of library and return a JSON formated for a bulk ELS request
Process artists in the track part of library and return a JSON formated for a bulk ELS request
"""
if 'Album Artist' not in track and 'Artist' not in track:
if "Album Artist" not in track and "Artist" not in track:
return
akey = track['Album Artist'] if 'Album Artist' in track else track['Artist']
# Add artist
if akey not in self._artists:
a_id = self.calc_id(akey)
akey = track["Album Artist"] if "Album Artist" in track else track["Artist"]
persistent_id = self.calc_id(akey)
if persistent_id not in self._artists:
# Key is used to increment/precise some information
# So we use artist name as a key to avoid calculating an ID for each track
self._artists[akey] = {
'Persistent ID': a_id,
'Name': akey,
'Artist': akey,
'Track Count': 0,
'Play Count': 0,
'Rating': 0,
'Genre': set(),
'Album': set()
}
# Compute information
play_count = track['Play Count'] if 'Play Count' in track else 0
rating = track['Rating'] if 'Rating' in track else 0
rating = self.calc_average(rating, self._artists[akey]['Rating'], self._artists[akey]['Track Count'])
self._artists[akey]['Track Count'] += 1
self._artists[akey]['Rating'] = rating
self._artists[akey]['Play Count'] += play_count
if 'Genre' in track:
# Split up the Genres
genre_parts = track['Genre'].split('/')
self._artists[akey]['Genre'] |= set(genre_parts)
if 'Album' in track:
self._artists[akey]['Album'].add(track['Album'])
def _process_album(self, track):
"""
Process albums in the track part of library and return a JSON formated for a bulk ELS request
"""
if 'Album' not in track:
return
akey = track['Album']
if akey not in self._albums:
a_id = self.calc_id(akey)
# Key is used to increment/precise some information
# So we use album name as a key to avoid calculating an ID for each track
self._albums[akey] = {
'Persistent ID': a_id,
'Name': akey,
'Album': akey,
'Track Count': 0,
'Play Count': 0,
'Genre': set(),
'Artist': set(),
'Avg Bit Rate': track['Bit Rate'],
'Min Bit Rate': track['Bit Rate'],
# 'Album Artist': '',
'Total Time': 0,
'Location': ''
self._artists[persistent_id] = {
"Persistent ID": persistent_id,
"Name": akey,
"Artist": akey,
"Track Count": 0,
"Play Count": 0,
"Rating": 0,
"Genre": set(),
"Album": set(),
}
# Compute information
play_count = track['Play Count'] if 'Play Count' in track else 0
play_count = track["Play Count"] if "Play Count" in track else 0
total_time = track['Total Time'] if 'Total Time' in track else 0
rating = track["Rating"] if "Rating" in track else 0
rating = self.calc_average(
rating,
self._artists[persistent_id]["Rating"],
self._artists[persistent_id]["Track Count"],
)
avg_bitrate = self.calc_average(track['Bit Rate'], self._albums[akey]['Avg Bit Rate'], self._albums[akey]['Track Count'])
self._artists[persistent_id]["Track Count"] += 1
self._artists[persistent_id]["Rating"] = rating
self._artists[persistent_id]["Play Count"] += play_count
self._albums[akey]['Avg Bit Rate'] = avg_bitrate
self._albums[akey]['Track Count'] += 1
self._albums[akey]['Play Count'] += play_count
self._albums[akey]['Total Time'] += total_time
self._albums[akey]['Location'] = os.path.dirname(track['Location'])
if self._albums[akey]['Min Bit Rate'] > track['Bit Rate']:
self._albums[akey]['Min Bit Rate'] = track['Bit Rate']
if 'Genre' in track:
if "Genre" in track:
# Split up the Genres
genre_parts = track['Genre'].split('/')
self._albums[akey]['Genre'] |= set(genre_parts)
genre_parts = track["Genre"].split("/")
self._artists[persistent_id]["Genre"] |= set(genre_parts)
if 'Artist' in track:
self._albums[akey]['Artist'].add(track['Artist'])
if "Album" in track:
self._artists[persistent_id]["Album"].add(track["Album"])
if 'Album Rating' in track:
self._albums[akey]['Album Rating'] = track['Album Rating']
def _process_album(self, track):
"""
Process albums in the track part of library and return a JSON formated for a bulk ELS request
"""
if "Album" not in track:
return
if 'Album Rating Computed' in track:
self._albums[akey]['Album Rating Computed'] = track['Album Rating Computed']
akey = track["Album"]
persistent_id = self.calc_id(akey)
if 'Album Artist' in track:
self._albums[akey]['Album Artist'] = track['Album Artist']
if persistent_id not in self._albums:
# Key is used to increment/precise some information
# So we use album name as a key to avoid calculating an ID for each track
self._albums[persistent_id] = {
"Persistent ID": persistent_id,
"Name": akey,
"Album": akey,
"Track Count": 0,
"Play Count": 0,
"Genre": set(),
"Artist": set(),
"Avg Bit Rate": track["Bit Rate"],
"Min Bit Rate": track["Bit Rate"],
# 'Album Artist': '',
"Total Time": 0,
"Location": "",
"Date Added": track["Date Added"],
}
# Compute information
play_count = track["Play Count"] if "Play Count" in track else 0
total_time = track["Total Time"] if "Total Time" in track else 0
avg_bitrate = self.calc_average(
track["Bit Rate"],
self._albums[persistent_id]["Avg Bit Rate"],
self._albums[persistent_id]["Track Count"],
)
self._albums[persistent_id]["Avg Bit Rate"] = avg_bitrate
self._albums[persistent_id]["Track Count"] += 1
self._albums[persistent_id]["Play Count"] += play_count
self._albums[persistent_id]["Total Time"] += total_time
self._albums[persistent_id]["Location"] = os.path.dirname(track["Location"])
if self._albums[persistent_id]["Min Bit Rate"] > track["Bit Rate"]:
self._albums[persistent_id]["Min Bit Rate"] = track["Bit Rate"]
if "Genre" in track:
# Split up the Genres
genre_parts = track["Genre"].split("/")
self._albums[persistent_id]["Genre"] |= set(genre_parts)
if "Artist" in track:
self._albums[persistent_id]["Artist"].add(track["Artist"])
if "Album Rating" in track:
self._albums[persistent_id]["Album Rating"] = track["Album Rating"]
if "Album Rating Computed" in track:
self._albums[persistent_id]["Album Rating Computed"] = track[
"Album Rating Computed"
]
if "Album Artist" in track:
self._albums[persistent_id]["Album Artist"] = track["Album Artist"]
if "Date Added" in track:
if track["Date Added"] < self._albums[persistent_id]["Date Added"]:
print(f"Lower date for {akey} - {track['Album']}")
self._albums[persistent_id]["Date Added"] = track["Date Added"]
@classmethod
def calc_average(cls, added_value, current_value, nb_values):
"""
Calculate average value from a current value, a value to add and the number of values
Calculate average value from a current value, a value to add and the number of values
"""
return (current_value * nb_values + added_value) / (nb_values + 1)
@classmethod
def calc_id(cls, key):
"""
Calculate a MD5 sum from a key as ID
Calculate a MD5 sum from a key as ID
"""
md5 = hashlib.md5()
md5.update(key.encode('UTF-8'))
md5.update(key.encode("UTF-8"))
return md5.hexdigest()
class WriteElsJson:
@staticmethod
def write_artists(artists, output_file):
def write_elements(
elements: list,
element_type: str,
json_style: str,
):
"""
Write artists data to another JSON file
Write songs to a JSON
"""
output_filename = f"es-{element_type}.{json_style}"
if json_style == "els":
output_filename += ".json"
file_artist = io.open(output_file, 'wb')
for _, artist in artists.items():
persistent_id = artist['Persistent ID']
artist['Rating'] = round(artist['Rating'])
json_dump_option = {"ensure_ascii": False, "cls": JsonCustomEncoder}
json_track_index = {
"index": {"_index": ITunesParser.ARTIST_INDEX, "_id": persistent_id}
}
with open(output_filename, "w", encoding="utf-8") as ofile:
match json_style:
case "json":
json_str = json.dumps(elements, **json_dump_option)
ofile.write(
json_str.replace("}, {", "},\n{")
) # One line = one record
case "jsonl":
for el in elements:
json.dump(el, ofile, **json_dump_option)
ofile.write("\n")
case "els":
for el in elements:
json_track_index = {
"index": {
"_index": f"itunes-{element_type}",
"_id": el["Persistent ID"],
}
}
json.dump(json_track_index, ofile, **json_dump_option)
ofile.write("\n")
json.dump(el, ofile, **json_dump_option)
ofile.write("\n")
case _:
print("ERROR: no write format")
file_artist.write(bytes(json.dumps(json_track_index, indent=None, cls=SetEncoder), 'UTF-8'))
file_artist.write(bytes("\n", 'UTF-8'))
file_artist.write(bytes(json.dumps(artist, indent=None, cls=SetEncoder), 'UTF-8'))
file_artist.write(bytes("\n", 'UTF-8'))
file_artist.close()
@staticmethod
def write_albums(albums, output_file):
"""
Write albums data to another JSON file
"""
file_albums = io.open(output_file, 'wb')
for _, album in albums.items():
persistent_id = album['Persistent ID']
album['Avg Bit Rate'] = round(album['Avg Bit Rate'])
json_track_index = {
"index": {"_index": ITunesParser.ALBUM_INDEX, "_id": persistent_id}
}
file_albums.write(bytes(json.dumps(json_track_index, indent=None, cls=SetEncoder), 'UTF-8'))
file_albums.write(bytes("\n", 'UTF-8'))
file_albums.write(bytes(json.dumps(album, indent=None, cls=SetEncoder), 'UTF-8'))
file_albums.write(bytes("\n", 'UTF-8'))
file_albums.close()
@staticmethod
def write_songs(songs, output_file):
"""
Write songs to a JSON
"""
file = io.open(output_file, 'wb')
for persistent_id, song in songs.items():
json_track_index = {
"index": {"_index": ITunesParser.SONG_INDEX, "_id": persistent_id}
}
file.write(bytes(json.dumps(json_track_index, indent=None, cls=SetEncoder), 'UTF-8'))
file.write(bytes("\n", 'UTF-8'))
file.write(bytes(json.dumps(song, indent=None, cls=SetEncoder), 'UTF-8'))
file.write(bytes("\n", 'UTF-8'))
file.close()
#### main block ####
# Default input & output files
DEFAULT_LIBRARY_FILE_NAME = 'iTunesLibrary.xml'
DEFAULT_OUTPUT_FILE_NAME = '/es-music-data.json'
DEFAULT_LIBRARY_FILE_NAME = "iTunesLibrary.xml"
DEFAULT_OUTPUT_FILE_NAME = "/es-music-data.json"
DEFAULT_LIBRARY_FILE = os.path.expanduser(DEFAULT_LIBRARY_FILE_NAME)
DEFAULT_OUTPUT_FILE = os.path.dirname(os.path.realpath(__file__)) + DEFAULT_OUTPUT_FILE_NAME
DEFAULT_OUTPUT_FILE = (
os.path.dirname(os.path.realpath(__file__)) + DEFAULT_OUTPUT_FILE_NAME
)
# Get options
parser = argparse.ArgumentParser(description="""
parser = argparse.ArgumentParser(
description="""
Parse an iTunes XML library file to produce JSON file for ELS bulk operation.
""")
parser.add_argument('-f', '--file', default=DEFAULT_LIBRARY_FILE,
help='iTunes Library XML file path (default: ./' + DEFAULT_LIBRARY_FILE_NAME + ')')
parser.add_argument('-o', '--output', default=DEFAULT_OUTPUT_FILE,
help='Output to file (default: .' + DEFAULT_OUTPUT_FILE_NAME + ')')
parser.add_argument('-c', '--console', action='store_true',
help='Output to console instead of file')
"""
)
parser.add_argument(
"-f",
"--file",
default=DEFAULT_LIBRARY_FILE,
help="iTunes Library XML file path (default: ./" + DEFAULT_LIBRARY_FILE_NAME + ")",
)
parser.add_argument(
"-o",
"--output",
default=DEFAULT_OUTPUT_FILE,
help="Output to file (default: ." + DEFAULT_OUTPUT_FILE_NAME + ")",
)
parser.add_argument(
"-c", "--console", action="store_true", help="Output to console instead of file"
)
parser.add_argument(
"-F",
"--format",
choices=["json", "jsonl", "els"],
default="json",
help="Choose JSON style",
)
# parser.add_argument('-v', '--verbose', action='store_true',
# help='Verbose output')
# help='Verbose output')
if __name__ == '__main__':
if __name__ == "__main__":
args = parser.parse_args()
print("Parsing file '{}'...".format(args.file))
print(f"Parsing file '{args.file}'...")
itunes_parser = ITunesParser().parse(args.file)
print("Writing JSON files...")
WriteElsJson.write_songs(itunes_parser['songs'], "es-songs.json")
WriteElsJson.write_artists(itunes_parser['artists'], "es-artists.json")
WriteElsJson.write_albums(itunes_parser['albums'], "es-albums.json")
print('Done!')
WriteElsJson.write_elements(
[x for _, x in itunes_parser["songs"].items()], "songs", args.format
)
WriteElsJson.write_elements(
[x for _, x in itunes_parser["artists"].items()], "artists", args.format
)
WriteElsJson.write_elements(
[x for _, x in itunes_parser["albums"].items()], "albums", args.format
)
print("Done!")
# if args.console:
# print(output)

95
ingest_cover.py Normal file
View File

@@ -0,0 +1,95 @@
import base64
import os
from typing import List, Optional
import requests
import json
import re
from urllib import parse
TYPESENSE_URL = "http://localhost:8108"
COLLECTION = "albums"
HEADERS = {"X-TYPESENSE-API-KEY": "toto", "Content-Type": "application/json"}
def get_albums(offset = 0) -> List[tuple[str, str, str]]:
params = {"q": "*", "per_page": 250, "offset": offset}
q_url = f"{TYPESENSE_URL}/collections/{COLLECTION}/documents/search"
# print(q_url)
req = requests.get(q_url, params=params, headers=HEADERS)
ret = [ (hit['document']['id'], hit['document']['Album'], hit['document']['Artist']) for hit in req.json()['hits']]
return ret
def get_album_location(id: int) -> str:
q_url = f"{TYPESENSE_URL}/collections/{COLLECTION}/documents/{id}"
# print(q_url)
req = requests.get(q_url, headers=HEADERS)
if "Cover" in req.json():
return
return req.json()["Location"]
def patch_album(id: int, cover: bytes) -> bool:
q_url = f"{TYPESENSE_URL}/collections/{COLLECTION}/documents/{id}"
# print(q_url)
payload = {"Cover": cover.decode()}
payload = json.dumps(payload)
req = requests.patch(q_url, headers=HEADERS, data=payload)
return req.status_code == 200
def sanitize_location(location: str) -> Optional[str]:
result = parse.unquote(location, encoding='utf-8', errors='replace')
result = result.replace("file://localhost/", "")
result = result.replace(":", "")
result = re.sub("^([A-Z])", lambda m: m.group(0).lower(), result)
result = result.replace("%20", " ")
result = "/mnt/" + result
result = re.sub('/Disc [0-9]*$', "", result)
for file in ["/Pochette.jpg", "/cover.jpg", "/Pochette.png"]:
if os.path.isfile(result + file):
result = result + file
return result
return None
def get_base64(location: str) -> bytes:
with open(location, "rb") as image_file:
encoded_cover = base64.b64encode(image_file.read())
return encoded_cover
def process_one_album(id: int) -> bool:
raw_loc = get_album_location(id)
if not raw_loc:
return False
loc = sanitize_location(raw_loc)
if not loc:
return False
cover = get_base64(loc)
if patch_album(id, cover):
return True
return False
if __name__ == "__main__":
# process_one_album(2151)
elements = get_albums()
processed = 0
while len(elements) == 250:
for id, name, artist in elements:
processed += 1
raw_loc = get_album_location(id)
if not raw_loc:
continue
loc = sanitize_location(raw_loc)
if not loc:
continue
print(f"Try {id} - {name} ({artist})")
cover = get_base64(loc)
if patch_album(id, cover):
print(f"Album {id} - {name} ({artist}) patched successfully")
elements = get_albums(offset = processed)

View File

@@ -0,0 +1 @@
3.13

0
navidrome/README.md Normal file
View File

78
navidrome/analyzer.py Normal file
View File

@@ -0,0 +1,78 @@
import sqlite3
from datetime import datetime
import meilisearch
def tzunit(dt) -> datetime:
if dt.tzinfo is not None:
return dt.replace(tzinfo=None)
return dt
client = meilisearch.Client("http://127.0.0.1:7700", "aSampleMasterKey")
index = client.index("itunes-albums")
con = sqlite3.connect("navidrome.db")
cur = con.cursor()
instertor = con.cursor()
update_statement = """UPDATE album SET
persistent_id = ?,
itunes_date = ?,
meili_nb_res = ?,
meili_title = ?,
meili_artist = ?,
meili_doc = ?,
meili_song_count = ?,
validated = ?
WHERE id = ?"""
for row in cur.execute("SELECT * FROM album;"):
msearch = index.search(
row[1], {"filter": [f'Artist="{row[2]}"'], "showRankingScore": True}
)
meili_result_len = len(msearch["hits"])
if meili_result_len > 0:
meili_hit = msearch["hits"][0]
if meili_hit["_rankingScore"] > 0.5:
meili_artist = (
[meili_hit["Album Artist"]]
if "Album Artist" in meili_hit
else meili_hit["Artist"]
)
nv_date = tzunit(datetime.fromisoformat(row[4]))
itunes_date = tzunit(datetime.fromisoformat(meili_hit["Date Added"]))
timedelta = nv_date - itunes_date
val_score = 0
if timedelta.days < 1:
val_score = -1
else:
if meili_result_len == 1:
val_score += 50
if meili_hit["Track Count"] == row[3]:
val_score += 40
if meili_hit["Name"] == row[1]:
val_score += 30
if row[2] in meili_artist:
val_score += 10
meili_artist = (
str(meili_artist[0]) if len(meili_artist) == 1 else str(meili_artist)
)
data = (
meili_hit["Persistent ID"],
meili_hit["Date Added"],
meili_result_len,
meili_hit["Name"],
str(meili_artist),
str(meili_hit),
meili_hit["Track Count"],
val_score,
row[0],
)
print(f"Update {row[1]}")
instertor.execute(update_statement, data)
con.commit()

View File

@@ -0,0 +1,41 @@
ALTER TABLE album DROP COLUMN 'size';
ALTER TABLE album DROP COLUMN 'embed_art_path';
ALTER TABLE album DROP COLUMN 'min_year';
ALTER TABLE album DROP COLUMN 'max_year';
ALTER TABLE album DROP COLUMN 'compilation';
ALTER TABLE album DROP COLUMN 'duration';
ALTER TABLE album DROP COLUMN 'genre';
ALTER TABLE album DROP COLUMN 'updated_at';
ALTER TABLE album DROP COLUMN 'full_text';
ALTER TABLE album DROP COLUMN 'album_artist_id';
ALTER TABLE album DROP COLUMN 'description';
ALTER TABLE album DROP COLUMN 'small_image_url';
ALTER TABLE album DROP COLUMN 'medium_image_url';
ALTER TABLE album DROP COLUMN 'large_image_url';
ALTER TABLE album DROP COLUMN 'external_url';
ALTER TABLE album DROP COLUMN 'external_info_updated_at';
ALTER TABLE album DROP COLUMN 'date';
ALTER TABLE album DROP COLUMN 'min_original_year';
ALTER TABLE album DROP COLUMN 'max_original_year';
ALTER TABLE album DROP COLUMN 'original_date';
ALTER TABLE album DROP COLUMN 'release_date';
ALTER TABLE album DROP COLUMN 'releases';
ALTER TABLE album DROP COLUMN 'order_album_name';
ALTER TABLE album DROP COLUMN 'order_album_artist_name';
ALTER TABLE album DROP COLUMN 'sort_album_name';
ALTER TABLE album DROP COLUMN 'sort_album_artist_name';
ALTER TABLE album DROP COLUMN 'catalog_num';
ALTER TABLE album DROP COLUMN 'comment';
ALTER TABLE album DROP COLUMN 'mbz_album_id';
ALTER TABLE album DROP COLUMN 'mbz_album_artist_id';
ALTER TABLE album DROP COLUMN 'mbz_album_type';
ALTER TABLE album DROP COLUMN 'mbz_album_comment';
ALTER TABLE album DROP COLUMN 'discs';
ALTER TABLE album DROP COLUMN 'library_id';
ALTER TABLE album DROP COLUMN 'imported_at';
ALTER TABLE album DROP COLUMN 'missing';
ALTER TABLE album DROP COLUMN 'mbz_release_group_id';
ALTER TABLE album DROP COLUMN 'tags';
ALTER TABLE album DROP COLUMN 'participants';
ALTER TABLE album DROP COLUMN 'folder_ids';
ALTER TABLE album DROP COLUMN 'explicit_status';

View File

@@ -0,0 +1,8 @@
ALTER TABLE album ADD COLUMN 'persistent_id' TEXT;
ALTER TABLE album ADD COLUMN 'itunes_date' TEXT;
ALTER TABLE album ADD COLUMN 'meili_nb_res' INTEGER;
ALTER TABLE album ADD COLUMN 'meili_title' TEXT;
ALTER TABLE album ADD COLUMN 'meili_artist' TEXT;
ALTER TABLE album ADD COLUMN 'meili_doc' BLOB;
ALTER TABLE album ADD COLUMN 'meili_song_count' INTEGER;
ALTER TABLE album ADD COLUMN 'validated' INTEGER;

21
navidrome/generator.py Normal file
View File

@@ -0,0 +1,21 @@
import sqlite3
from datetime import datetime
def tzunit(dt) -> datetime:
if dt.tzinfo is not None:
return dt.replace(tzinfo=None)
return dt
con = sqlite3.connect("navidrome.db")
cur = con.cursor()
for row in cur.execute("SELECT * FROM album where persistent_id is not null;"):
if row[12] >= 100:
nv_date = tzunit(datetime.fromisoformat(row[4]))
itunes_date = tzunit(datetime.fromisoformat(row[6]))
timedelta = nv_date - itunes_date
if timedelta.days > 1:
sql_st = f"UPDATE album SET created_at = '{itunes_date.isoformat()}' WHERE id = '{row[0]}';"
print(sql_st)

23
navidrome/init_db.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
cp ~/workspace/litestream/navidrome/navidrome.db .
echo
echo "Removing tables..."
for table in $(sqlite3 navidrome.db "SELECT name FROM sqlite_master WHERE name != 'album' and type='table'"); do
echo Process $table
sqlite3 navidrome.db "DROP TABLE $table;" || echo "Failed for table $table"
done
echo
echo "Removing index..."
for index in $(sqlite3 navidrome.db "SELECT name FROM sqlite_master WHERE type='index'"); do
echo $index
sqlite3 navidrome.db "DROP INDEX $index;" || echo "Failed for index $index"
done
echo
echo "Apply scripts..."
sqlite3 navidrome.db < clean_cols.sql.sql
sqlite3 navidrome.db < create_cols.sql

9
navidrome/pyproject.toml Normal file
View File

@@ -0,0 +1,9 @@
[project]
name = "navidrome"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"meilisearch>=0.37.0",
]

218
navidrome/uv.lock generated Normal file
View File

@@ -0,0 +1,218 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "camel-converter"
version = "5.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a5/dd/f8df35e1d2b6360a27c1613937bde2f68db41b8a67cd856f105d2031a3ed/camel_converter-5.0.0.tar.gz", hash = "sha256:97d0cf15a75b40abab288c526c71143b468376dd61690fd1cb0a49d9e471f111", size = 58926, upload-time = "2025-10-07T18:01:04.577Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/67/283543fc3ac6d6d8e9cd710e331ebf2323a284f66be304ac786c3b7bdeb4/camel_converter-5.0.0-py3-none-any.whl", hash = "sha256:5a64900bcba1ca047c504ee88cefa905a5bcac15cec06f6d6f9d1ebad46d4e80", size = 6254, upload-time = "2025-10-07T18:01:03.248Z" },
]
[package.optional-dependencies]
pydantic = [
{ name = "pydantic" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "meilisearch"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "camel-converter", extra = ["pydantic"] },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/03/4b/36c1c288b1485c9a7a507ea139f1c65a1dee66ae60e820a1437507a3d899/meilisearch-0.37.0.tar.gz", hash = "sha256:a8c0e947d9029b26e833d91ba818cdd56be478756521ce8fe6bcc3178e916ed7", size = 28646, upload-time = "2025-08-20T07:48:57.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/25/ff33395a9c34960e7f875eec70bc75d80bb53a75acb743f9963e53254e9f/meilisearch-0.37.0-py3-none-any.whl", hash = "sha256:313d5046fffaec4d89480298d3680e64d3c1e7c7577b7f6765a092fcf56d8571", size = 29165, upload-time = "2025-08-20T07:48:56.263Z" },
]
[[package]]
name = "navidrome"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "meilisearch" },
]
[package.metadata]
requires-dist = [{ name = "meilisearch", specifier = ">=0.37.0" }]
[[package]]
name = "pydantic"
version = "2.12.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]