mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
implement backup and restore draft 1
+ add extra fields for backup in favorites and scrobble data - not yet for the playlist tracks
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
- New table to hold similar artist entries
|
||||
- Create 2 way relationships, such that if an artist A is similar to another B with a certain weight,
|
||||
then artist B is similar to A with the same weight, unless overwritten.
|
||||
- Clean up tempfiles after transcoding
|
||||
|
||||
# Bug fixes
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from app.api import (
|
||||
getall,
|
||||
auth,
|
||||
stream,
|
||||
backup_and_restore
|
||||
)
|
||||
|
||||
# TODO: Move this description to a separate file
|
||||
@@ -107,6 +108,7 @@ def create_api():
|
||||
app.register_api(settings.api)
|
||||
app.register_api(colors.api)
|
||||
app.register_api(lyrics.api)
|
||||
app.register_api(backup_and_restore.api)
|
||||
|
||||
# Plugins
|
||||
app.register_api(plugins.api)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
from dataclasses import asdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from time import time
|
||||
from flask_openapi3 import Tag
|
||||
from flask_openapi3 import APIBlueprint
|
||||
from app.api.auth import admin_required
|
||||
|
||||
from app.db.userdata import FavoritesTable, PlaylistTable, ScrobbleTable
|
||||
from app.settings import Paths
|
||||
|
||||
bp_tag = Tag(name="Backup and Restore", description="Backup and Restore")
|
||||
api = APIBlueprint("backup_and_restore", __name__, url_prefix="/", abp_tags=[bp_tag])
|
||||
|
||||
|
||||
@api.post("/backup")
|
||||
@admin_required()
|
||||
def backup():
|
||||
"""
|
||||
Create a backup file of your favorites, playlists and scrobble data.
|
||||
"""
|
||||
backup_dir = Path(Paths.get_app_dir()) / "backup"
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
backup_name = f"backup.{int(time())}"
|
||||
backup_file = backup_dir / f"{backup_name}.json"
|
||||
|
||||
# INFO: Image folder for playlist images
|
||||
img_folder = backup_dir / "images" / backup_name
|
||||
img_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
favorites = FavoritesTable.get_all()
|
||||
favorites = [asdict(entry) for entry in favorites]
|
||||
|
||||
scrobbles = ScrobbleTable.get_all(start=0)
|
||||
scrobbles = [asdict(entry) for entry in scrobbles]
|
||||
|
||||
# SECTION: Playlists
|
||||
playlists = PlaylistTable.get_all()
|
||||
playlist_dicts = []
|
||||
|
||||
for entry in playlists:
|
||||
playlist = asdict(entry)
|
||||
for key in ["_last_updated", "has_image", "images", "duration", "count"]:
|
||||
del playlist[key]
|
||||
|
||||
playlist_dicts.append(playlist)
|
||||
|
||||
# copy images
|
||||
if playlist["thumb"]:
|
||||
img_path = Path(Paths.get_playlist_img_path()) / playlist["thumb"]
|
||||
shutil.copy(img_path, img_folder / playlist["thumb"])
|
||||
|
||||
# !SECTION
|
||||
|
||||
data = {
|
||||
"favorites": favorites,
|
||||
"scrobbles": scrobbles,
|
||||
"playlists": playlist_dicts,
|
||||
}
|
||||
|
||||
with open(backup_file, "w") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
return {
|
||||
"msg": "Backup created",
|
||||
"data_path": str(backup_file),
|
||||
"images_path": str(img_folder),
|
||||
}, 200
|
||||
|
||||
|
||||
@api.post("/restore")
|
||||
@admin_required()
|
||||
def restore():
|
||||
"""
|
||||
Restore your favorites, playlists and scrobble data from a backup file.
|
||||
"""
|
||||
return {"msg": "Restore"}
|
||||
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
|
||||
from app.api.apischemas import GenericLimitSchema
|
||||
from app.db.libdata import TrackTable
|
||||
from app.db.userdata import FavoritesTable
|
||||
from app.lib.extras import get_extra_info
|
||||
from app.models import FavType
|
||||
from app.settings import Defaults
|
||||
|
||||
@@ -71,9 +72,12 @@ def toggle_favorite(body: FavoritesAddBody):
|
||||
"""
|
||||
Adds a favorite to the database.
|
||||
"""
|
||||
extra = get_extra_info(body.hash, body.type)
|
||||
|
||||
try:
|
||||
FavoritesTable.insert_item({"hash": body.hash, "type": body.type})
|
||||
FavoritesTable.insert_item(
|
||||
{"hash": body.hash, "type": body.type, "extra": extra}
|
||||
)
|
||||
except:
|
||||
return {"msg": "Failed! An error occured"}, 500
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from pydantic import Field
|
||||
from app.api.apischemas import TrackHashSchema
|
||||
|
||||
from app.db.userdata import ScrobbleTable
|
||||
from app.lib.extras import get_extra_info
|
||||
from app.settings import Defaults
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.artists import ArtistStore
|
||||
@@ -39,7 +40,9 @@ def log_track(body: LogTrackBody):
|
||||
if trackentry is None:
|
||||
return {"msg": "Track not found."}, 404
|
||||
|
||||
ScrobbleTable.add(dict(body))
|
||||
scrobble_data = dict(body)
|
||||
scrobble_data["extra"] = get_extra_info(body.trackhash, "track")
|
||||
ScrobbleTable.add(scrobble_data)
|
||||
|
||||
# Update play data on the in-memory stores
|
||||
track = trackentry.tracks[0]
|
||||
|
||||
+4
-17
@@ -247,23 +247,11 @@ def send_file_as_chunks(filepath: str) -> Response:
|
||||
while remaining_bytes > 0 or retry_count < max_retries:
|
||||
if retry_count == max_retries:
|
||||
print("💚 sending final chunk! ...")
|
||||
return (
|
||||
file.read(os.path.getsize(filepath) - file.tell()),
|
||||
file.tell(),
|
||||
True,
|
||||
)
|
||||
print("\n\n")
|
||||
print(f"file: {filepath}")
|
||||
print(f"start: {start}")
|
||||
print(f"end: {end}")
|
||||
print(f"filesize: {os.path.getsize(filepath)}")
|
||||
print(f"⭐ (O) Remaining bytes: {remaining_bytes}")
|
||||
print(f"⭐ Remaining bytes: {remaining_bytes}")
|
||||
print(f"⭐ Cursor position: {file.tell()}")
|
||||
# Read the chunk size or all the remaining bytes
|
||||
|
||||
print(f"💚 remaining_bytes: {remaining_bytes}")
|
||||
print(f"💚 retry_count: {retry_count}")
|
||||
pos = file.tell()
|
||||
chunk = file.read(os.path.getsize(filepath) - pos)
|
||||
|
||||
return chunk, pos, True
|
||||
|
||||
if remaining_bytes < chunk_size:
|
||||
time.sleep(0.25)
|
||||
@@ -303,7 +291,6 @@ def send_file_as_chunks(filepath: str) -> Response:
|
||||
f"bytes {start}-{position}/{os.path.getsize(filepath) + bytes_to_add}",
|
||||
)
|
||||
response.headers.add("Accept-Ranges", "bytes")
|
||||
response.headers.add("Content-Length", str(len(data or [])))
|
||||
return response
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -266,7 +266,7 @@ class ScrobbleTable(Base):
|
||||
return cls.insert_one(item)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, start: int, limit: int | None):
|
||||
def get_all(cls, start: int, limit: int | None = None):
|
||||
result = cls.execute(
|
||||
select(cls)
|
||||
.where(cls.userid == get_current_userid())
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
from typing import Any
|
||||
from app.store.albums import AlbumStore
|
||||
from app.store.artists import ArtistStore
|
||||
from app.store.tracks import TrackStore
|
||||
|
||||
|
||||
def get_extra_info(hash: str, type: str):
|
||||
"""
|
||||
Generates extra info for a track, album or artist, which will be stored
|
||||
in the database (in favorites, playlists and scrobble data) for backup and restore.
|
||||
|
||||
The extra info contains all the fields needed to reconstruct the itemhash. The track contains an additional filepath field which can be used to locate the file when restoring.
|
||||
"""
|
||||
extra: dict[str, Any] = {}
|
||||
|
||||
if type == "track":
|
||||
trackentry = TrackStore.trackhashmap.get(hash)
|
||||
if trackentry is not None:
|
||||
track = trackentry.get_best()
|
||||
|
||||
extra["filepath"] = track.filepath
|
||||
extra["title"] = track.title
|
||||
extra["artists"] = [a["name"] for a in track.artists]
|
||||
extra["album"] = track.albumhash
|
||||
|
||||
elif type == "album":
|
||||
album = AlbumStore.get_album_by_hash(hash)
|
||||
if album is not None:
|
||||
extra["albumartists"] = [a["name"] for a in album.albumartists]
|
||||
extra["title"] = album.title
|
||||
|
||||
elif type == "artist":
|
||||
artist = ArtistStore.get_artist_by_hash(hash)
|
||||
if artist is not None:
|
||||
extra["name"] = artist.name
|
||||
|
||||
return extra
|
||||
Reference in New Issue
Block a user