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:
cwilvx
2024-08-17 12:19:24 +03:00
parent ca31054f48
commit 7852be5e3f
8 changed files with 133 additions and 21 deletions
+2 -1
View File
@@ -43,10 +43,11 @@
- New table to hold similar artist entries - 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, - 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. then artist B is similar to A with the same weight, unless overwritten.
- Clean up tempfiles after transcoding
# Bug fixes # Bug fixes
- Duplicates on search - Duplicates on search
- Audio stops on ending - Audio stops on ending
- Show users on account settings when logged in as admin and show users on login is disabled. - Show users on account settings when logged in as admin and show users on login is disabled.
- Save both filepath and trackhash in favorites and playlists - Save both filepath and trackhash in favorites and playlists
+2
View File
@@ -31,6 +31,7 @@ from app.api import (
getall, getall,
auth, auth,
stream, stream,
backup_and_restore
) )
# TODO: Move this description to a separate file # TODO: Move this description to a separate file
@@ -107,6 +108,7 @@ def create_api():
app.register_api(settings.api) app.register_api(settings.api)
app.register_api(colors.api) app.register_api(colors.api)
app.register_api(lyrics.api) app.register_api(lyrics.api)
app.register_api(backup_and_restore.api)
# Plugins # Plugins
app.register_api(plugins.api) app.register_api(plugins.api)
+78
View File
@@ -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"}
+5 -1
View File
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from app.api.apischemas import GenericLimitSchema from app.api.apischemas import GenericLimitSchema
from app.db.libdata import TrackTable from app.db.libdata import TrackTable
from app.db.userdata import FavoritesTable from app.db.userdata import FavoritesTable
from app.lib.extras import get_extra_info
from app.models import FavType from app.models import FavType
from app.settings import Defaults from app.settings import Defaults
@@ -71,9 +72,12 @@ def toggle_favorite(body: FavoritesAddBody):
""" """
Adds a favorite to the database. Adds a favorite to the database.
""" """
extra = get_extra_info(body.hash, body.type)
try: try:
FavoritesTable.insert_item({"hash": body.hash, "type": body.type}) FavoritesTable.insert_item(
{"hash": body.hash, "type": body.type, "extra": extra}
)
except: except:
return {"msg": "Failed! An error occured"}, 500 return {"msg": "Failed! An error occured"}, 500
+4 -1
View File
@@ -4,6 +4,7 @@ from pydantic import Field
from app.api.apischemas import TrackHashSchema from app.api.apischemas import TrackHashSchema
from app.db.userdata import ScrobbleTable from app.db.userdata import ScrobbleTable
from app.lib.extras import get_extra_info
from app.settings import Defaults from app.settings import Defaults
from app.store.albums import AlbumStore from app.store.albums import AlbumStore
from app.store.artists import ArtistStore from app.store.artists import ArtistStore
@@ -39,7 +40,9 @@ def log_track(body: LogTrackBody):
if trackentry is None: if trackentry is None:
return {"msg": "Track not found."}, 404 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 # Update play data on the in-memory stores
track = trackentry.tracks[0] track = trackentry.tracks[0]
+4 -17
View File
@@ -247,23 +247,11 @@ def send_file_as_chunks(filepath: str) -> Response:
while remaining_bytes > 0 or retry_count < max_retries: while remaining_bytes > 0 or retry_count < max_retries:
if retry_count == max_retries: if retry_count == max_retries:
print("💚 sending final chunk! ...") 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}") pos = file.tell()
print(f"💚 retry_count: {retry_count}") chunk = file.read(os.path.getsize(filepath) - pos)
return chunk, pos, True
if remaining_bytes < chunk_size: if remaining_bytes < chunk_size:
time.sleep(0.25) 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}", f"bytes {start}-{position}/{os.path.getsize(filepath) + bytes_to_add}",
) )
response.headers.add("Accept-Ranges", "bytes") response.headers.add("Accept-Ranges", "bytes")
response.headers.add("Content-Length", str(len(data or [])))
return response return response
+1 -1
View File
@@ -266,7 +266,7 @@ class ScrobbleTable(Base):
return cls.insert_one(item) return cls.insert_one(item)
@classmethod @classmethod
def get_all(cls, start: int, limit: int | None): def get_all(cls, start: int, limit: int | None = None):
result = cls.execute( result = cls.execute(
select(cls) select(cls)
.where(cls.userid == get_current_userid()) .where(cls.userid == get_current_userid())
+37
View File
@@ -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