diff --git a/backend/appsettings/serializers.py b/backend/appsettings/serializers.py
index 70564465..6da5ae1f 100644
--- a/backend/appsettings/serializers.py
+++ b/backend/appsettings/serializers.py
@@ -100,6 +100,13 @@ class PoTokenSerializer(serializers.Serializer):
potoken = serializers.CharField()
+class RescanFileSystemConfig(serializers.Serializer):
+ """serialize rescan filesystem config"""
+
+ ignore_error = serializers.BooleanField()
+ prefer_local = serializers.BooleanField()
+
+
class SnapshotItemSerializer(serializers.Serializer):
"""serialize snapshot response"""
diff --git a/backend/appsettings/src/filesystem.py b/backend/appsettings/src/filesystem.py
index 0a609289..13324331 100644
--- a/backend/appsettings/src/filesystem.py
+++ b/backend/appsettings/src/filesystem.py
@@ -5,11 +5,13 @@ Functionality:
import os
+from appsettings.src.config import AppConfig
from common.src.env_settings import EnvironmentSettings
from common.src.es_connect import IndexPaginate
-from common.src.helper import ignore_filelist
+from common.src.helper import ignore_filelist, rand_sleep
from video.src.comments import Comments
from video.src.index import YoutubeVideo, index_new_video
+from video.src.meta_embed import IndexFromEmbed
class Scanner:
@@ -17,19 +19,27 @@ class Scanner:
VIDEOS: str = EnvironmentSettings.MEDIA_DIR
- def __init__(self, task=False) -> None:
+ def __init__(
+ self,
+ task=False,
+ ignore_error: bool = False,
+ prefer_local: bool = False,
+ ) -> None:
self.task = task
- self.to_delete: set[str] = set()
- self.to_index: set[str] = set()
+ self.ignore_error = ignore_error
+ self.prefer_local = prefer_local
+ self.config = None
+ self.to_delete: set[tuple[str, str]] = set()
+ self.to_index: set[tuple[str, str]] = set()
def scan(self) -> None:
"""scan the filesystem"""
- downloaded: set[str] = self._get_downloaded()
- indexed: set[str] = self._get_indexed()
+ downloaded = self._get_downloaded()
+ indexed = self._get_indexed()
self.to_index = downloaded - indexed
self.to_delete = indexed - downloaded
- def _get_downloaded(self) -> set[str]:
+ def _get_downloaded(self) -> set[tuple[str, str]]:
"""get downloaded ids"""
if self.task:
self.task.send_progress(["Scan your filesystem for videos."])
@@ -39,28 +49,40 @@ class Scanner:
for channel in channels:
folder = os.path.join(self.VIDEOS, channel)
files = ignore_filelist(os.listdir(folder))
- downloaded.update({i.split(".")[0] for i in files})
+ downloaded.update(
+ {
+ (i.split(".")[0], f"{channel}/{i}")
+ for i in files
+ if i.endswith(".mp4")
+ }
+ )
return downloaded
- def _get_indexed(self) -> set:
+ def _get_indexed(self) -> set[tuple[str, str]]:
"""get all indexed ids"""
if self.task:
self.task.send_progress(["Get all videos indexed."])
- data = {"query": {"match_all": {}}, "_source": ["youtube_id"]}
+ data = {
+ "query": {"match_all": {}},
+ "_source": ["youtube_id", "media_url"],
+ }
response = IndexPaginate("ta_video", data).get_results()
- return {i["youtube_id"] for i in response}
+ return {(i["youtube_id"], i["media_url"]) for i in response}
def apply(self) -> None:
"""apply all changes"""
+ if not self.config:
+ self.config = AppConfig().config
+
self.delete()
self.index()
def delete(self) -> None:
"""delete videos from index"""
if not self.to_delete:
- print("nothing to delete")
+ print("[scanner] nothing to delete")
return
if self.task:
@@ -68,25 +90,69 @@ class Scanner:
[f"Remove {len(self.to_delete)} videos from index."]
)
- for youtube_id in self.to_delete:
+ for youtube_id, _ in self.to_delete:
YoutubeVideo(youtube_id).delete_media_file()
def index(self) -> None:
"""index new"""
if not self.to_index:
- print("nothing to index")
+ print("[scanner] nothing to index")
return
total = len(self.to_index)
- for idx, youtube_id in enumerate(self.to_index):
- if self.task:
- self.task.send_progress(
- message_lines=[
- f"Index missing video {youtube_id}, {idx + 1}/{total}"
- ],
- progress=(idx + 1) / total,
- )
- json_data = index_new_video(youtube_id)
- if json_data:
+ for idx, (youtube_id, media_url) in enumerate(self.to_index):
+ self._notify(total, youtube_id, idx)
+
+ file_path = os.path.join(self.VIDEOS, media_url)
+ if self.prefer_local:
+ # try index from embed
+ json_data = IndexFromEmbed(
+ file_path, use_user_conf=True, config=self.config
+ ).run_index()
+ if json_data:
+ continue
+
+ try:
+ # try index from remote
+ json_data = index_new_video(youtube_id)
Comments(youtube_id).build_json(upload=True)
YoutubeVideo(youtube_id).embed_metadata()
+ rand_sleep(self.config)
+ except ValueError as err:
+ # fallback from index from embed
+ json_data = IndexFromEmbed(
+ file_path, use_user_conf=True, config=self.config
+ ).run_index()
+ if json_data:
+ continue
+
+ if self.ignore_error:
+ self._notify_error(youtube_id)
+ rand_sleep(self.config)
+ continue
+
+ raise ValueError from err
+
+ def _notify(self, total, youtube_id, idx):
+ """send notification"""
+ if not self.task:
+ return
+
+ self.task.send_progress(
+ message_lines=[
+ f"Index missing video {youtube_id}, {idx + 1}/{total}"
+ ],
+ progress=(idx + 1) / total,
+ )
+
+ def _notify_error(self, youtube_id):
+ """notify error"""
+ if not self.task:
+ return
+
+ message = f"[scanner] Failed to index {youtube_id}, no metadata"
+ print(f"[scanner] {message}")
+ self.task.send_progress(
+ message_lines=[message, "Continue..."],
+ level="error",
+ )
diff --git a/backend/appsettings/urls.py b/backend/appsettings/urls.py
index 4dde4188..d4f96d4f 100644
--- a/backend/appsettings/urls.py
+++ b/backend/appsettings/urls.py
@@ -44,6 +44,11 @@ urlpatterns = [
views.TokenView.as_view(),
name="api-token",
),
+ path(
+ "rescan-filesystem/",
+ views.RescanFileSystem.as_view(),
+ name="api-rescan-filesystem",
+ ),
path(
"membership/profile/",
views_mb.MembershipProfileView.as_view(),
diff --git a/backend/appsettings/views.py b/backend/appsettings/views.py
index edb87ca3..cb909219 100644
--- a/backend/appsettings/views.py
+++ b/backend/appsettings/views.py
@@ -6,6 +6,7 @@ from appsettings.serializers import (
CookieUpdateSerializer,
CookieValidationSerializer,
PoTokenSerializer,
+ RescanFileSystemConfig,
SnapshotCreateResponseSerializer,
SnapshotItemSerializer,
SnapshotListSerializer,
@@ -291,7 +292,11 @@ class CookieView(ApiBaseView):
class POTokenView(ApiBaseView):
- """handle PO token"""
+ """resolves to /api/appsettings/potoken/
+ GET: get potoken
+ POST: update potoken
+ DELETE: revoke potoken
+ """
permission_classes = [AdminOnly]
@@ -388,6 +393,30 @@ class SnapshotApiListView(ApiBaseView):
return Response(serializer.data)
+class RescanFileSystem(ApiBaseView):
+ """resolves to /api/appsettings/rescan-filesystem/
+ POST: start new task rescan filesystem task
+ """
+
+ @staticmethod
+ @extend_schema(
+ request=RescanFileSystemConfig,
+ responses={
+ 200: OpenApiResponse(AsyncTaskResponseSerializer()),
+ },
+ )
+ def post(request):
+ """start new task rescan filesystem task"""
+ data_serializer = RescanFileSystemConfig(data=request.data)
+ data_serializer.is_valid(raise_exception=True)
+ validated_data = data_serializer.validated_data
+
+ message = TaskCommand().start("rescan_filesystem", validated_data)
+ serializer = AsyncTaskResponseSerializer(message)
+
+ return Response(serializer.data)
+
+
class SnapshotApiView(ApiBaseView):
"""resolves to /api/appsettings/snapshot/
- Rescan your media folder looking for missing videos and clean up index. More info on the - Github{' '} + Rescan your media folder looking for missing videos and clean up index. More info on the{' '} - Wiki + Docs .
Prefer embedded metadata
+Ignore missing metadata errors
+File system scan in progress
} {!reScanningFileSystem && (