Frontend caching reload fix, #build

Changed:
- Fix frontend caching issue
- Better published date handling and parsing with mapping fixes
- bump yt-dlp
This commit is contained in:
Simon 2025-10-27 17:11:53 +07:00
commit 0e6149cc50
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
13 changed files with 560 additions and 450 deletions

View File

@ -434,6 +434,10 @@
}
}
},
"published": {
"type": "date",
"format": "epoch_second||strict_date_optional_time"
},
"vid_thumb_url": {
"type": "keyword"
},

View File

@ -0,0 +1,13 @@
"""middleware"""
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
class StartTimeMiddleware(MiddlewareMixin):
"""add a start time header"""
def __call__(self, request):
response = self.get_response(request)
response["X-Start-Timestamp"] = settings.TA_START
return response

View File

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import hashlib
from datetime import datetime
from os import environ, path
from pathlib import Path
@ -76,6 +77,7 @@ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"config.middleware.StartTimeMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@ -220,10 +222,13 @@ CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = list(default_headers) + [
"mode",
]
CORS_EXPOSE_HEADERS = ["X-Start-Timestamp"]
# TA application settings
TA_UPSTREAM = "https://github.com/tubearchivist/tubearchivist"
TA_VERSION = "v0.5.8-unstable"
TA_START = str(int(datetime.now().timestamp()))
# API
REST_FRAMEWORK = {

View File

@ -153,7 +153,7 @@ class PendingList(PendingIndex):
if entry["type"] == "video":
to_add = self._add_video(entry["url"], entry["vid_type"])
if to_add:
self.__notify_add(
self._notify_add(
item_type="video",
name=to_add["title"],
idx=idx,
@ -218,7 +218,7 @@ class PendingList(PendingIndex):
total = len(video_results)
for idx, video_data in enumerate(video_results, start=1):
to_add = self.__parse_channel_video(
to_add = self._parse_channel_video(
video_data, vid_type, channel_handler.json_data
)
if self.task and self.task.is_stopped():
@ -228,14 +228,14 @@ class PendingList(PendingIndex):
continue
self.missing_videos.append(to_add)
self.__notify_add(
self._notify_add(
item_type="channel",
name=channel_handler.json_data["channel_name"],
idx=idx,
total=total,
)
def __parse_channel_video(
def _parse_channel_video(
self, video_data, vid_type, channel_json
) -> dict | None:
"""parse video of channel"""
@ -300,7 +300,7 @@ class PendingList(PendingIndex):
continue
self.missing_videos.append(to_add)
self.__notify_add(
self._notify_add(
item_type="playlist",
name=playlist.json_data["playlist_name"],
idx=idx,
@ -368,11 +368,11 @@ class PendingList(PendingIndex):
to_add = {
"youtube_id": video_data["id"],
"title": video_data["title"],
"vid_thumb_url": self.__extract_thumb(video_data),
"vid_thumb_url": self._extract_thumb(video_data),
"duration": get_duration_str(video_data.get("duration", 0)),
"published": self.__extract_published(video_data),
"published": self._extract_published(video_data),
"timestamp": int(datetime.now().timestamp()),
"vid_type": self.__extract_vid_type(video_data),
"vid_type": self._extract_vid_type(video_data),
"channel_name": video_data["channel"],
"channel_id": video_data["channel_id"],
"channel_indexed": video_data["channel_id"] in self.all_channels,
@ -380,7 +380,7 @@ class PendingList(PendingIndex):
return to_add
def __extract_thumb(self, video_data) -> str | None:
def _extract_thumb(self, video_data) -> str | None:
"""extract thumb"""
if "thumbnail" in video_data:
return video_data["thumbnail"]
@ -390,7 +390,7 @@ class PendingList(PendingIndex):
return None
def __extract_published(self, video_data) -> str | int | None:
def _extract_published(self, video_data) -> str | int | None:
"""build published date or timestamp"""
timestamp = video_data.get("timestamp")
if timestamp:
@ -405,7 +405,7 @@ class PendingList(PendingIndex):
return published
def __extract_vid_type(self, video_data) -> str:
def _extract_vid_type(self, video_data) -> str:
"""build vid type"""
if (
"vid_type" in video_data
@ -464,7 +464,7 @@ class PendingList(PendingIndex):
return len(self.missing_videos)
def __notify_add(
def _notify_add(
self, item_type: str, name: str, idx: int, total: int
) -> None:
"""notify"""

View File

@ -1,15 +1,15 @@
apprise==1.9.4
apprise==1.9.5
celery==5.5.3
django-auth-ldap==5.2.0
django-celery-beat==2.8.1
django-cors-headers==4.9.0
Django==5.2.6
Django==5.2.7
djangorestframework==3.16.1
drf-spectacular==0.28.0
Pillow==11.3.0
redis==6.4.0
Pillow==12.0.0
redis==7.0.0
requests==2.32.5
ryd-client==0.0.6
uvicorn==0.37.0
uvicorn==0.38.0
whitenoise==6.11.0
yt-dlp[default]==2025.9.26
yt-dlp[default]==2025.10.22

View File

@ -222,6 +222,10 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
return timestamp
upload_date = self.youtube_meta["upload_date"]
if not upload_date:
raise ValueError(
f"Could not extract published date for {self.youtube_id}"
)
upload_date_time = datetime.strptime(upload_date, "%Y%m%d")
published = upload_date_time.strftime("%Y-%m-%d")

View File

@ -50,17 +50,21 @@ server {
root /app/static;
index index.html;
location ~* .(?:css|js)$ {
location ~* ^/(?!static/|cache/).*\.(?:css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
try_files $uri $uri/ /index.html =404;
}
location = /index.html {
add_header Cache-Control 'no-store';
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
expires 0;
}
location / {
add_header Cache-Control 'no-store';
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
try_files $uri $uri/ /index.html =404;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import getFetchCredentials from '../configuration/getFetchCredentials';
import logOut from '../api/actions/logOut';
import getCookie from './getCookie';
import Routes from '../configuration/routes/RouteList';
import { useBackendStore } from '../stores/BackendStore';
export interface ApiClientOptions extends Omit<RequestInit, 'body'> {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@ -44,6 +45,12 @@ const APIClient = async <T>(
...options,
});
const backendTimestamp = response.headers.get('X-Start-Timestamp');
if (backendTimestamp) {
const { setStartTimestamp } = useBackendStore.getState();
setStartTimestamp(backendTimestamp);
}
// Handle common errors
if (response.status === 400) {
const data = await response.json();

View File

@ -396,19 +396,31 @@ const SettingsApplication = () => {
<span className="settings-current">
{'bestvideo[height<=720]+bestaudio/best[height<=720]'}
</span>
: best audio and max video height of 720p.
: <q>best</q> video and <q>best</q> audio (yt-dlp's choice), max video
height of 720p.
</li>
<li>
<span className="settings-current">
{'bestvideo[height<=1080]+bestaudio/best[height<=1080]'}
</span>
: best audio and max video height of 1080p.
: <q>best</q> video and <q>best</q> audio (yt-dlp's choice), max video
height of 1080p.
</li>
<li>
<span className="settings-current">
{'bestvideo[height<=1080][vcodec*=avc1]+bestaudio[acodec*=mp4a]/mp4'}
</span>
: Max 1080p video height with iOS compatible video and audio codecs.
: <q>best</q> universally iOS-compatible video (forced avc1) and{' '}
<q>best</q> audio (forced mp4a), mp4 container, max video height of 1080p.
</li>
<li>
<span className="settings-current">
'bv*[vcodec~=av01]+ba[acodec~=mp4a]/bv*[vcodec~=av01]+ba/
bv*[vcodec~=avc1]+ba[acodec~=mp4a]/bv*[vcodec~=avc1]+ba/bv*+ba/b'
</span>
: <q>best</q> iOS-compatible video (av01, compatible with iPhone 15 Pro
and newer) and <q>best</q> audio (mp4a), no max video height, with
fallback to avc1 and other formats if necessary.
</li>
<li>This can also be configured on a per channel basis.</li>
<li>

View File

@ -0,0 +1,18 @@
import { create } from 'zustand';
interface BackendState {
startTimestamp: string | null;
setStartTimestamp: (timestamp: string) => void;
}
export const useBackendStore = create<BackendState>((set, get) => ({
startTimestamp: null,
setStartTimestamp: timestamp => {
const prev = get().startTimestamp;
if (prev && prev !== timestamp) {
console.warn('Backend restart detected — reloading frontend...');
window.location.reload();
}
set({ startTimestamp: timestamp });
},
}));

View File

@ -8,14 +8,22 @@ export default defineConfig({
base: '/',
server: {
host: true,
port: 3000, // This is the port which we will use in docker
// Thanks @sergiomoura for the window fix
// add the next lines if you're using windows and hot reload doesn't work
port: 3000,
watch: {
usePolling: true,
},
},
build: {
sourcemap: true,
outDir: 'dist',
assetsDir: 'assets',
manifest: true,
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]',
},
},
},
});

View File

@ -1,10 +1,10 @@
-r backend/requirements.txt
ipython==9.5.0
ipython==9.6.0
pre-commit==4.3.0
pylint-django==2.6.1
pylint==3.3.8
pylint==3.3.9
pytest-django==4.11.1
pytest==8.4.2
python-dotenv==1.1.1
python-dotenv==1.2.1
requirementscheck==0.1.0
types-requests==2.32.4.20250913