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,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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.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,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
@@ -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
@@ -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())
|
||||||
|
|||||||
@@ -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