Feat: add yt-dlp version checker
This commit is contained in:
parent
042379035a
commit
00c3aefff1
10 changed files with 389 additions and 175 deletions
|
|
@ -24,12 +24,18 @@ class UpdateChecker(metaclass=Singleton):
|
||||||
GITHUB_API_URL: str = "https://api.github.com/repos/arabcoders/ytptube/releases/latest"
|
GITHUB_API_URL: str = "https://api.github.com/repos/arabcoders/ytptube/releases/latest"
|
||||||
"GitHub API endpoint for latest release"
|
"GitHub API endpoint for latest release"
|
||||||
|
|
||||||
|
YTDLP_API_URL: str = "https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest"
|
||||||
|
"GitHub API endpoint for yt-dlp latest release"
|
||||||
|
|
||||||
CACHE_DURATION: int = 300
|
CACHE_DURATION: int = 300
|
||||||
"Cache duration in seconds (5 minutes)"
|
"Cache duration in seconds (5 minutes)"
|
||||||
|
|
||||||
CACHE_KEY: str = "update_checker:result"
|
CACHE_KEY: str = "update_checker:result"
|
||||||
"Cache key for storing check results"
|
"Cache key for storing check results"
|
||||||
|
|
||||||
|
YTDLP_CACHE_KEY: str = "update_checker:ytdlp"
|
||||||
|
"Cache key for storing yt-dlp check results"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, config: Config | None = None, scheduler: Scheduler | None = None, notify: EventBus | None = None
|
self, config: Config | None = None, scheduler: Scheduler | None = None, notify: EventBus | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -42,7 +48,7 @@ class UpdateChecker(metaclass=Singleton):
|
||||||
self._notify: EventBus = notify or EventBus.get_instance()
|
self._notify: EventBus = notify or EventBus.get_instance()
|
||||||
"Instance of EventBus for notifications."
|
"Instance of EventBus for notifications."
|
||||||
|
|
||||||
self._cache: Cache = Cache.get_instance()
|
self._cache: Cache = Cache()
|
||||||
"Instance of Cache for caching check results."
|
"Instance of Cache for caching check results."
|
||||||
|
|
||||||
self._job_id: str | None = None
|
self._job_id: str | None = None
|
||||||
|
|
@ -52,28 +58,9 @@ class UpdateChecker(metaclass=Singleton):
|
||||||
def get_instance(
|
def get_instance(
|
||||||
config: Config | None = None, scheduler: Scheduler | None = None, notify: EventBus | None = None
|
config: Config | None = None, scheduler: Scheduler | None = None, notify: EventBus | None = None
|
||||||
) -> "UpdateChecker":
|
) -> "UpdateChecker":
|
||||||
"""
|
|
||||||
Get the singleton instance of UpdateChecker.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config (Config | None): Optional Config instance to use.
|
|
||||||
scheduler (Scheduler | None): Optional Scheduler instance to use.
|
|
||||||
notify (EventBus | None): Optional EventBus instance to use.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
UpdateChecker: The singleton instance of UpdateChecker.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return UpdateChecker(config=config, scheduler=scheduler, notify=notify)
|
return UpdateChecker(config=config, scheduler=scheduler, notify=notify)
|
||||||
|
|
||||||
def attach(self, _: web.Application) -> None:
|
def attach(self, _: web.Application) -> None:
|
||||||
"""
|
|
||||||
Attach the UpdateChecker to the application.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
_ (web.Application): The aiohttp web application instance.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from .Services import Services
|
from .Services import Services
|
||||||
|
|
||||||
Services.get_instance().add("update_checker", self)
|
Services.get_instance().add("update_checker", self)
|
||||||
|
|
@ -97,7 +84,7 @@ class UpdateChecker(metaclass=Singleton):
|
||||||
|
|
||||||
self._schedule_check()
|
self._schedule_check()
|
||||||
|
|
||||||
async def on_shutdown(self, _: web.Application) -> None:
|
async def on_shutdown(self, _: web.Application | None) -> None:
|
||||||
"""
|
"""
|
||||||
Handle application shutdown event.
|
Handle application shutdown event.
|
||||||
|
|
||||||
|
|
@ -110,89 +97,129 @@ class UpdateChecker(metaclass=Singleton):
|
||||||
|
|
||||||
self._scheduler.remove(self._job_id)
|
self._scheduler.remove(self._job_id)
|
||||||
self._job_id = None
|
self._job_id = None
|
||||||
LOG.debug("Stopped update check scheduled task.")
|
|
||||||
|
|
||||||
def _schedule_check(self) -> None:
|
def _schedule_check(self) -> None:
|
||||||
"""Schedule the update check task to run daily at 3 AM."""
|
|
||||||
if not self._config.check_for_updates:
|
if not self._config.check_for_updates:
|
||||||
LOG.debug("Update checking is disabled, skipping scheduling.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Run daily at 3 AM
|
|
||||||
timer: str = "0 3 * * *"
|
timer: str = "0 3 * * *"
|
||||||
|
|
||||||
self._job_id = self._scheduler.add(
|
self._job_id = self._scheduler.add(
|
||||||
timer=timer,
|
timer=timer,
|
||||||
func=lambda: asyncio.create_task(self.check_for_updates()),
|
func=lambda: asyncio.create_task(self.check_for_updates()),
|
||||||
id="update_checker",
|
id=f"{__class__.__name__}.{self.check_for_updates.__name__}",
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG.info(f"Scheduled update check to run daily at 3 AM (cron: {timer}).")
|
async def check_for_updates(self) -> tuple[tuple[str, str | None], tuple[str, str | None]]:
|
||||||
|
|
||||||
async def check_for_updates(self) -> tuple[str, str | None]:
|
|
||||||
"""
|
"""
|
||||||
Check for updates from GitHub releases.
|
Check for updates from GitHub releases for both the app and yt-dlp.
|
||||||
Updates config.new_version if a newer version is available.
|
Updates config.new_version and config.yt_new_version if newer versions are available.
|
||||||
Stops the scheduled task if an update is found.
|
Stops the scheduled task if an app update is found.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple[str, str | None]: (status, new_version)
|
tuple[tuple[str, str | None], tuple[str, str | None]]: ((app_status, app_version), (ytdlp_status, ytdlp_version))
|
||||||
status: "disabled", "error", "up_to_date", or "update_available"
|
status: "disabled", "error", "up_to_date", or "update_available"
|
||||||
new_version: The new version tag if available, None otherwise
|
version: The new version tag if available, None otherwise
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self._config.check_for_updates:
|
if not self._config.check_for_updates:
|
||||||
LOG.debug("Update checking is disabled, skipping check.")
|
return (("disabled", None), ("disabled", None))
|
||||||
return ("disabled", None)
|
|
||||||
|
|
||||||
# Check cache
|
app_cached = await self._cache.aget(self.CACHE_KEY)
|
||||||
cached = await self._cache.aget(self.CACHE_KEY)
|
ytdlp_cached = await self._cache.aget(self.YTDLP_CACHE_KEY)
|
||||||
if cached:
|
|
||||||
ttl = await self._cache.attl(self.CACHE_KEY)
|
|
||||||
LOG.debug(f"Returning cached result (TTL: {ttl:.0f}s)")
|
|
||||||
return cached
|
|
||||||
|
|
||||||
|
if app_cached and ytdlp_cached:
|
||||||
|
return (app_cached, ytdlp_cached)
|
||||||
|
|
||||||
|
app_result = app_cached if app_cached else await self._check_app_version()
|
||||||
|
ytdlp_result = ytdlp_cached if ytdlp_cached else await self._check_ytdlp_version()
|
||||||
|
|
||||||
|
return (app_result, ytdlp_result)
|
||||||
|
|
||||||
|
async def _check_github_version(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
api_url: str,
|
||||||
|
current_version: str,
|
||||||
|
cache_key: str,
|
||||||
|
strip_v_prefix: bool = False,
|
||||||
|
) -> tuple[str, str | None]:
|
||||||
try:
|
try:
|
||||||
LOG.info("Checking for application updates...")
|
LOG.info(f"Checking for {name} updates...")
|
||||||
|
|
||||||
current_version: str = APP_VERSION.lstrip("v")
|
|
||||||
|
|
||||||
async with async_client(timeout=10.0) as client:
|
async with async_client(timeout=10.0) as client:
|
||||||
response = await client.get(
|
response = await client.get(
|
||||||
self.GITHUB_API_URL,
|
api_url,
|
||||||
headers={"Accept": "application/vnd.github+json"},
|
headers={"Accept": "application/vnd.github+json"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if 200 != response.status_code:
|
if 200 != response.status_code:
|
||||||
LOG.warning(f"Failed to check for updates: HTTP {response.status_code}")
|
LOG.warning(f"Failed to check for {name} updates: HTTP {response.status_code}")
|
||||||
return ("error", None)
|
return ("error", None)
|
||||||
|
|
||||||
data: dict[str, Any] = response.json()
|
data: dict[str, Any] = response.json()
|
||||||
|
|
||||||
latest_tag: str = data.get("tag_name", "").lstrip("v")
|
latest_tag: str = data.get("tag_name", "")
|
||||||
if not latest_tag:
|
if not latest_tag:
|
||||||
LOG.warning("No tag_name found in GitHub release data.")
|
LOG.warning(f"No tag_name found in {name} GitHub release data.")
|
||||||
return ("error", None)
|
return ("error", None)
|
||||||
|
|
||||||
if self._compare_versions(current_version, latest_tag):
|
compare_current: str = current_version.lstrip("v") if strip_v_prefix else current_version
|
||||||
LOG.warning(f"Update available: {current_version} → {latest_tag}")
|
compare_latest: str = latest_tag.lstrip("v") if strip_v_prefix else latest_tag
|
||||||
new_version_tag = data.get("tag_name", "")
|
|
||||||
self._config.new_version = new_version_tag
|
if self._compare_versions(compare_current, compare_latest):
|
||||||
await self.on_shutdown(None)
|
LOG.warning(f"{name} update available: {current_version} -> {latest_tag}")
|
||||||
result = ("update_available", new_version_tag)
|
result: tuple[str, str] = ("update_available", latest_tag)
|
||||||
await self._cache.aset(self.CACHE_KEY, result, self.CACHE_DURATION)
|
await self._cache.aset(cache_key, result, self.CACHE_DURATION)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
LOG.info("No updates available.")
|
LOG.info(f"No {name} updates available.")
|
||||||
self._config.new_version = ""
|
result: tuple[str, None] = ("up_to_date", None)
|
||||||
result = ("up_to_date", None)
|
await self._cache.aset(cache_key, result, self.CACHE_DURATION)
|
||||||
await self._cache.aset(self.CACHE_KEY, result, self.CACHE_DURATION)
|
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
LOG.error(f"Error checking for updates: {e!s}")
|
LOG.error(f"Error checking for {name} updates: {e!s}")
|
||||||
return ("error", None)
|
return ("error", None)
|
||||||
|
|
||||||
|
async def _check_app_version(self) -> tuple[str, str | None]:
|
||||||
|
status, new_version = await self._check_github_version(
|
||||||
|
name="application",
|
||||||
|
api_url=self.GITHUB_API_URL,
|
||||||
|
current_version=APP_VERSION,
|
||||||
|
cache_key=self.CACHE_KEY,
|
||||||
|
strip_v_prefix=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if "update_available" == status:
|
||||||
|
self._config.new_version = new_version or ""
|
||||||
|
await self.on_shutdown(None)
|
||||||
|
elif "up_to_date" == status:
|
||||||
|
self._config.new_version = ""
|
||||||
|
|
||||||
|
return (status, new_version)
|
||||||
|
|
||||||
|
async def _check_ytdlp_version(self) -> tuple[str, str | None]:
|
||||||
|
current_version: str = self._config._ytdlp_version()
|
||||||
|
if not current_version or "0.0.0" == current_version:
|
||||||
|
LOG.warning("Could not determine yt-dlp version, skipping yt-dlp update check.")
|
||||||
|
return ("error", None)
|
||||||
|
|
||||||
|
status, new_version = await self._check_github_version(
|
||||||
|
name="yt-dlp",
|
||||||
|
api_url=self.YTDLP_API_URL,
|
||||||
|
current_version=current_version,
|
||||||
|
cache_key=self.YTDLP_CACHE_KEY,
|
||||||
|
strip_v_prefix=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if "update_available" == status:
|
||||||
|
self._config.yt_new_version = new_version or ""
|
||||||
|
elif "up_to_date" == status:
|
||||||
|
self._config.yt_new_version = ""
|
||||||
|
|
||||||
|
return (status, new_version)
|
||||||
|
|
||||||
def _compare_versions(self, current: str, latest: str) -> bool:
|
def _compare_versions(self, current: str, latest: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Compare version strings to determine if an update is available.
|
Compare version strings to determine if an update is available.
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,9 @@ class Config(metaclass=Singleton):
|
||||||
new_version: str = ""
|
new_version: str = ""
|
||||||
"The new version available."
|
"The new version available."
|
||||||
|
|
||||||
|
yt_new_version: str = ""
|
||||||
|
"The new yt-dlp version available."
|
||||||
|
|
||||||
_manual_vars: tuple = (
|
_manual_vars: tuple = (
|
||||||
"temp_path",
|
"temp_path",
|
||||||
"config_path",
|
"config_path",
|
||||||
|
|
@ -255,6 +258,7 @@ class Config(metaclass=Singleton):
|
||||||
"app_build_date",
|
"app_build_date",
|
||||||
"app_branch",
|
"app_branch",
|
||||||
"new_version",
|
"new_version",
|
||||||
|
"yt_new_version",
|
||||||
)
|
)
|
||||||
"The variables that are immutable."
|
"The variables that are immutable."
|
||||||
|
|
||||||
|
|
@ -327,6 +331,7 @@ class Config(metaclass=Singleton):
|
||||||
"default_pagination",
|
"default_pagination",
|
||||||
"check_for_updates",
|
"check_for_updates",
|
||||||
"new_version",
|
"new_version",
|
||||||
|
"yt_new_version",
|
||||||
)
|
)
|
||||||
"The variables that are relevant to the frontend."
|
"The variables that are relevant to the frontend."
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,33 +193,38 @@ async def check_updates(config: Config, encoder: Encoder, update_checker: Update
|
||||||
Response: The response object.
|
Response: The response object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not config.check_for_updates:
|
if config.new_version or config.yt_new_version:
|
||||||
return web.json_response(
|
|
||||||
{"error": "Update checking is disabled in configuration."},
|
|
||||||
status=web.HTTPBadRequest.status_code,
|
|
||||||
dumps=encoder.encode,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If update already found, return cached result
|
|
||||||
if config.new_version:
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
data={
|
data={
|
||||||
"status": "update_available",
|
"app": {
|
||||||
"current_version": config.app_version,
|
"status": "update_available" if config.new_version else "up_to_date",
|
||||||
"new_version": config.new_version,
|
"current_version": config.app_version,
|
||||||
|
"new_version": config.new_version if config.new_version else None,
|
||||||
|
},
|
||||||
|
"ytdlp": {
|
||||||
|
"status": "update_available" if config.yt_new_version else "up_to_date",
|
||||||
|
"current_version": config._ytdlp_version(),
|
||||||
|
"new_version": config.yt_new_version if config.yt_new_version else None,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
status=web.HTTPOk.status_code,
|
status=web.HTTPOk.status_code,
|
||||||
dumps=encoder.encode,
|
dumps=encoder.encode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run check and await result
|
(app_status, app_new_version), (ytdlp_status, ytdlp_new_version) = await update_checker.check_for_updates()
|
||||||
status, new_version = await update_checker.check_for_updates()
|
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
data={
|
data={
|
||||||
"status": status,
|
"app": {
|
||||||
"current_version": config.app_version,
|
"status": app_status,
|
||||||
"new_version": new_version,
|
"current_version": config.app_version,
|
||||||
|
"new_version": app_new_version,
|
||||||
|
},
|
||||||
|
"ytdlp": {
|
||||||
|
"status": ytdlp_status,
|
||||||
|
"current_version": config._ytdlp_version(),
|
||||||
|
"new_version": ytdlp_new_version,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
status=web.HTTPOk.status_code,
|
status=web.HTTPOk.status_code,
|
||||||
dumps=encoder.encode,
|
dumps=encoder.encode,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class TestCheckUpdatesEndpoint:
|
||||||
|
|
||||||
response = await check_updates(config, encoder, update_checker)
|
response = await check_updates(config, encoder, update_checker)
|
||||||
|
|
||||||
assert 400 == response.status, "Should return 400 when update checking is disabled"
|
assert 200 == response.status, "Should work even if disabled as it's manual check."
|
||||||
body = response.body
|
body = response.body
|
||||||
assert b"disabled" in body.lower(), "Response should mention update checking is disabled"
|
assert b"disabled" in body.lower(), "Response should mention update checking is disabled"
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ class TestCheckUpdatesEndpoint:
|
||||||
update_checker = UpdateChecker.get_instance()
|
update_checker = UpdateChecker.get_instance()
|
||||||
|
|
||||||
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
||||||
mock_check.return_value = ("up_to_date", None)
|
mock_check.return_value = (("up_to_date", None), ("up_to_date", None))
|
||||||
response = await check_updates(config, encoder, update_checker)
|
response = await check_updates(config, encoder, update_checker)
|
||||||
|
|
||||||
assert 200 == response.status, "Should return 200"
|
assert 200 == response.status, "Should return 200"
|
||||||
|
|
@ -57,7 +57,7 @@ class TestCheckUpdatesEndpoint:
|
||||||
update_checker = UpdateChecker.get_instance()
|
update_checker = UpdateChecker.get_instance()
|
||||||
|
|
||||||
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
||||||
mock_check.return_value = ("up_to_date", None)
|
mock_check.return_value = (("up_to_date", None), ("up_to_date", None))
|
||||||
response = await check_updates(config, encoder, update_checker)
|
response = await check_updates(config, encoder, update_checker)
|
||||||
|
|
||||||
assert 200 == response.status, "Should return 200"
|
assert 200 == response.status, "Should return 200"
|
||||||
|
|
@ -74,7 +74,7 @@ class TestCheckUpdatesEndpoint:
|
||||||
update_checker = UpdateChecker.get_instance()
|
update_checker = UpdateChecker.get_instance()
|
||||||
|
|
||||||
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
||||||
mock_check.return_value = ("update_available", "v1.0.5")
|
mock_check.return_value = (("update_available", "v1.0.5"), ("up_to_date", None))
|
||||||
response = await check_updates(config, encoder, update_checker)
|
response = await check_updates(config, encoder, update_checker)
|
||||||
|
|
||||||
assert 200 == response.status, "Should return 200"
|
assert 200 == response.status, "Should return 200"
|
||||||
|
|
@ -92,7 +92,7 @@ class TestCheckUpdatesEndpoint:
|
||||||
update_checker = UpdateChecker.get_instance()
|
update_checker = UpdateChecker.get_instance()
|
||||||
|
|
||||||
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
||||||
mock_check.return_value = ("up_to_date", None)
|
mock_check.return_value = (("up_to_date", None), ("up_to_date", None))
|
||||||
response = await check_updates(config, encoder, update_checker)
|
response = await check_updates(config, encoder, update_checker)
|
||||||
|
|
||||||
assert 200 == response.status, "Should return 200"
|
assert 200 == response.status, "Should return 200"
|
||||||
|
|
@ -109,7 +109,7 @@ class TestCheckUpdatesEndpoint:
|
||||||
update_checker = UpdateChecker.get_instance()
|
update_checker = UpdateChecker.get_instance()
|
||||||
|
|
||||||
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
with patch.object(update_checker, "check_for_updates", new_callable=AsyncMock) as mock_check:
|
||||||
mock_check.return_value = ("error", None)
|
mock_check.return_value = (("error", None), ("error", None))
|
||||||
response = await check_updates(config, encoder, update_checker)
|
response = await check_updates(config, encoder, update_checker)
|
||||||
|
|
||||||
assert 200 == response.status, "Should return 200"
|
assert 200 == response.status, "Should return 200"
|
||||||
|
|
|
||||||
|
|
@ -53,23 +53,19 @@ class TestUpdateChecker:
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
config.check_for_updates = True
|
config.check_for_updates = True
|
||||||
|
|
||||||
# Create scheduler with a fresh event loop
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
scheduler = Scheduler.get_instance(loop=loop)
|
scheduler = Scheduler.get_instance(loop=loop)
|
||||||
notify = EventBus.get_instance()
|
notify = EventBus.get_instance()
|
||||||
|
|
||||||
# Mock APP_VERSION to be a release version
|
|
||||||
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
||||||
checker = UpdateChecker.get_instance(config=config, scheduler=scheduler)
|
checker = UpdateChecker.get_instance(config=config, scheduler=scheduler)
|
||||||
app_mock = MagicMock()
|
app_mock = MagicMock()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
checker.attach(app_mock)
|
checker.attach(app_mock)
|
||||||
|
|
||||||
assert checker._job_id is not None, "Should have scheduled a job"
|
assert checker._job_id is not None, "Should have scheduled a job"
|
||||||
assert "update_checker" == checker._job_id, "Job ID should be 'update_checker'"
|
assert "UpdateChecker.check_for_updates" == checker._job_id, "Job ID should be 'update_checker'"
|
||||||
finally:
|
finally:
|
||||||
# Clean up
|
|
||||||
if checker._job_id:
|
if checker._job_id:
|
||||||
scheduler.remove(checker._job_id)
|
scheduler.remove(checker._job_id)
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
@ -118,7 +114,6 @@ class TestUpdateChecker:
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
config.check_for_updates = True
|
config.check_for_updates = True
|
||||||
|
|
||||||
# Create scheduler with a fresh event loop
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
scheduler = Scheduler.get_instance(loop=loop)
|
scheduler = Scheduler.get_instance(loop=loop)
|
||||||
notify = EventBus.get_instance()
|
notify = EventBus.get_instance()
|
||||||
|
|
@ -126,15 +121,11 @@ class TestUpdateChecker:
|
||||||
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
||||||
checker = UpdateChecker.get_instance(config=config, scheduler=scheduler)
|
checker = UpdateChecker.get_instance(config=config, scheduler=scheduler)
|
||||||
app_mock = MagicMock()
|
app_mock = MagicMock()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
checker.attach(app_mock)
|
checker.attach(app_mock)
|
||||||
initial_job_id = checker._job_id
|
initial_job_id = checker._job_id
|
||||||
|
|
||||||
assert initial_job_id is not None, "Should have job ID before shutdown"
|
assert initial_job_id is not None, "Should have job ID before shutdown"
|
||||||
|
|
||||||
await checker.on_shutdown(app_mock)
|
await checker.on_shutdown(app_mock)
|
||||||
|
|
||||||
assert checker._job_id is None, "Should clear job ID after shutdown"
|
assert checker._job_id is None, "Should clear job ID after shutdown"
|
||||||
finally:
|
finally:
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
@ -148,15 +139,18 @@ class TestUpdateChecker:
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
config.check_for_updates = False
|
config.check_for_updates = False
|
||||||
config.new_version = ""
|
config.new_version = ""
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
checker = UpdateChecker.get_instance(config=config)
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
|
|
||||||
# Should return disabled status
|
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
|
||||||
status, new_version = await checker.check_for_updates()
|
|
||||||
|
|
||||||
assert "disabled" == status, "Should return disabled status"
|
assert "disabled" == app_status, "Should return disabled status for app"
|
||||||
assert new_version is None, "Should return None for new_version when disabled"
|
assert app_version is None, "Should return None for app new_version when disabled"
|
||||||
|
assert "disabled" == ytdlp_status, "Should return disabled status for yt-dlp"
|
||||||
|
assert ytdlp_version is None, "Should return None for yt-dlp new_version when disabled"
|
||||||
assert "" == config.new_version, "Should not update new_version when disabled"
|
assert "" == config.new_version, "Should not update new_version when disabled"
|
||||||
|
assert "" == config.yt_new_version, "Should not update yt_new_version when disabled"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch("app.library.UpdateChecker.async_client")
|
@patch("app.library.UpdateChecker.async_client")
|
||||||
|
|
@ -168,25 +162,32 @@ class TestUpdateChecker:
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
config.check_for_updates = True
|
config.check_for_updates = True
|
||||||
config.new_version = ""
|
config.new_version = ""
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
# Mock HTTP response
|
mock_app_response = MagicMock()
|
||||||
mock_response = MagicMock()
|
mock_app_response.status_code = 200
|
||||||
mock_response.status_code = 200
|
mock_app_response.json.return_value = {"tag_name": "v99.0.0"}
|
||||||
mock_response.json.return_value = {"tag_name": "v99.0.0"}
|
|
||||||
|
|
||||||
|
mock_ytdlp_response = MagicMock()
|
||||||
|
mock_ytdlp_response.status_code = 200
|
||||||
|
mock_ytdlp_response.json.return_value = {"tag_name": "9999.12.31"}
|
||||||
|
|
||||||
|
mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response])
|
||||||
mock_context = AsyncMock()
|
mock_context = AsyncMock()
|
||||||
mock_context.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
mock_context.__aenter__.return_value.get = mock_get
|
||||||
mock_client.return_value = mock_context
|
mock_client.return_value = mock_context
|
||||||
|
|
||||||
# Mock current version to be older
|
|
||||||
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
||||||
checker = UpdateChecker.get_instance(config=config)
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
status, new_version = await checker.check_for_updates()
|
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
|
||||||
|
|
||||||
assert "update_available" == status, "Should return update_available status"
|
assert "update_available" == app_status, "Should return update_available status for app"
|
||||||
assert "v99.0.0" == new_version, "Should return new version tag"
|
assert "v99.0.0" == app_version, "Should return new app version tag"
|
||||||
assert "v99.0.0" == config.new_version, "Should store new version tag"
|
assert "v99.0.0" == config.new_version, "Should store new app version tag"
|
||||||
assert checker._job_id is None, "Should stop scheduled task after finding update"
|
assert "update_available" == ytdlp_status, "Should return update_available status for yt-dlp"
|
||||||
|
assert "9999.12.31" == ytdlp_version, "Should return new yt-dlp version tag"
|
||||||
|
assert "9999.12.31" == config.yt_new_version, "Should store new yt-dlp version tag"
|
||||||
|
assert checker._job_id is None, "Should stop scheduled task after finding app update"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch("app.library.UpdateChecker.async_client")
|
@patch("app.library.UpdateChecker.async_client")
|
||||||
|
|
@ -198,24 +199,32 @@ class TestUpdateChecker:
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
config.check_for_updates = True
|
config.check_for_updates = True
|
||||||
config.new_version = ""
|
config.new_version = ""
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
# Mock HTTP response with older version (current is hardcoded as 1.0.14 in the code)
|
mock_app_response = MagicMock()
|
||||||
mock_response = MagicMock()
|
mock_app_response.status_code = 200
|
||||||
mock_response.status_code = 200
|
mock_app_response.json.return_value = {"tag_name": "v1.0.0"}
|
||||||
mock_response.json.return_value = {"tag_name": "v1.0.0"}
|
|
||||||
|
|
||||||
|
mock_ytdlp_response = MagicMock()
|
||||||
|
mock_ytdlp_response.status_code = 200
|
||||||
|
mock_ytdlp_response.json.return_value = {"tag_name": "2020.01.01"}
|
||||||
|
|
||||||
|
mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response])
|
||||||
mock_context = AsyncMock()
|
mock_context = AsyncMock()
|
||||||
mock_context.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
mock_context.__aenter__.return_value.get = mock_get
|
||||||
mock_client.return_value = mock_context
|
mock_client.return_value = mock_context
|
||||||
|
|
||||||
checker = UpdateChecker.get_instance(config=config)
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
checker._job_id = "test-job" # Set a job ID to verify it's not removed
|
checker._job_id = "test-job"
|
||||||
|
|
||||||
status, new_version = await checker.check_for_updates()
|
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
|
||||||
|
|
||||||
assert "up_to_date" == status, "Should return up_to_date status"
|
assert "up_to_date" == app_status, "Should return up_to_date status for app"
|
||||||
assert new_version is None, "Should return None for new_version"
|
assert app_version is None, "Should return None for app new_version"
|
||||||
assert "" == config.new_version, "Should clear new_version when no update available"
|
assert "" == config.new_version, "Should clear new_version when no app update available"
|
||||||
|
assert "up_to_date" == ytdlp_status, "Should return up_to_date status for yt-dlp"
|
||||||
|
assert ytdlp_version is None, "Should return None for yt-dlp new_version"
|
||||||
|
assert "" == config.yt_new_version, "Should clear yt_new_version when no yt-dlp update available"
|
||||||
assert "test-job" == checker._job_id, "Should keep scheduled task running"
|
assert "test-job" == checker._job_id, "Should keep scheduled task running"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -228,8 +237,8 @@ class TestUpdateChecker:
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
config.check_for_updates = True
|
config.check_for_updates = True
|
||||||
config.new_version = ""
|
config.new_version = ""
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
# Mock HTTP error
|
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.status_code = 404
|
mock_response.status_code = 404
|
||||||
|
|
||||||
|
|
@ -239,11 +248,14 @@ class TestUpdateChecker:
|
||||||
|
|
||||||
checker = UpdateChecker.get_instance(config=config)
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
|
|
||||||
status, new_version = await checker.check_for_updates()
|
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
|
||||||
|
|
||||||
assert "error" == status, "Should return error status"
|
assert "error" == app_status, "Should return error status for app"
|
||||||
assert new_version is None, "Should return None for new_version on error"
|
assert app_version is None, "Should return None for app new_version on error"
|
||||||
assert "" == config.new_version, "Should not set new_version on HTTP error"
|
assert "" == config.new_version, "Should not set new_version on HTTP error"
|
||||||
|
assert "error" == ytdlp_status, "Should return error status for yt-dlp"
|
||||||
|
assert ytdlp_version is None, "Should return None for yt-dlp new_version on error"
|
||||||
|
assert "" == config.yt_new_version, "Should not set yt_new_version on HTTP error"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch("app.library.UpdateChecker.async_client")
|
@patch("app.library.UpdateChecker.async_client")
|
||||||
|
|
@ -255,19 +267,22 @@ class TestUpdateChecker:
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
config.check_for_updates = True
|
config.check_for_updates = True
|
||||||
config.new_version = ""
|
config.new_version = ""
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
# Mock exception during HTTP request
|
|
||||||
mock_context = AsyncMock()
|
mock_context = AsyncMock()
|
||||||
mock_context.__aenter__.return_value.get = AsyncMock(side_effect=Exception("Network error"))
|
mock_context.__aenter__.return_value.get = AsyncMock(side_effect=Exception("Network error"))
|
||||||
mock_client.return_value = mock_context
|
mock_client.return_value = mock_context
|
||||||
|
|
||||||
checker = UpdateChecker.get_instance(config=config)
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
|
|
||||||
status, new_version = await checker.check_for_updates()
|
(app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates()
|
||||||
|
|
||||||
assert "error" == status, "Should return error status on exception"
|
assert "error" == app_status, "Should return error status on exception for app"
|
||||||
assert new_version is None, "Should return None for new_version on exception"
|
assert app_version is None, "Should return None for app new_version on exception"
|
||||||
assert "" == config.new_version, "Should not set new_version on exception"
|
assert "" == config.new_version, "Should not set new_version on exception"
|
||||||
|
assert "error" == ytdlp_status, "Should return error status on exception for yt-dlp"
|
||||||
|
assert ytdlp_version is None, "Should return None for yt-dlp new_version on exception"
|
||||||
|
assert "" == config.yt_new_version, "Should not set yt_new_version on exception"
|
||||||
|
|
||||||
def test_compare_versions_newer_available(self):
|
def test_compare_versions_newer_available(self):
|
||||||
"""Test version comparison detects newer version."""
|
"""Test version comparison detects newer version."""
|
||||||
|
|
@ -311,13 +326,19 @@ class TestUpdateChecker:
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
config.check_for_updates = True
|
config.check_for_updates = True
|
||||||
config.new_version = ""
|
config.new_version = ""
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
mock_response = MagicMock()
|
mock_app_response = MagicMock()
|
||||||
mock_response.status_code = 200
|
mock_app_response.status_code = 200
|
||||||
mock_response.json.return_value = {"tag_name": "v2.0.0"}
|
mock_app_response.json.return_value = {"tag_name": "v2.0.0"}
|
||||||
|
|
||||||
|
mock_ytdlp_response = MagicMock()
|
||||||
|
mock_ytdlp_response.status_code = 200
|
||||||
|
mock_ytdlp_response.json.return_value = {"tag_name": "2026.01.01"}
|
||||||
|
|
||||||
|
mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response])
|
||||||
mock_context = AsyncMock()
|
mock_context = AsyncMock()
|
||||||
mock_context.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
mock_context.__aenter__.return_value.get = mock_get
|
||||||
|
|
||||||
with patch("app.library.UpdateChecker.async_client", return_value=mock_context):
|
with patch("app.library.UpdateChecker.async_client", return_value=mock_context):
|
||||||
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
||||||
|
|
@ -325,6 +346,7 @@ class TestUpdateChecker:
|
||||||
await checker.check_for_updates()
|
await checker.check_for_updates()
|
||||||
|
|
||||||
assert "v2.0.0" == config.new_version, "Should store full tag_name including 'v' prefix"
|
assert "v2.0.0" == config.new_version, "Should store full tag_name including 'v' prefix"
|
||||||
|
assert "2026.01.01" == config.yt_new_version, "Should store yt-dlp tag_name"
|
||||||
|
|
||||||
def test_subscribe_to_started_event(self):
|
def test_subscribe_to_started_event(self):
|
||||||
"""Test that attach subscribes to Events.STARTED."""
|
"""Test that attach subscribes to Events.STARTED."""
|
||||||
|
|
@ -350,7 +372,6 @@ class TestUpdateChecker:
|
||||||
|
|
||||||
checker.attach(app_mock)
|
checker.attach(app_mock)
|
||||||
|
|
||||||
# Verify subscription was created
|
|
||||||
subscriptions = notify._listeners.get("started", [])
|
subscriptions = notify._listeners.get("started", [])
|
||||||
assert len(subscriptions) > 0, "Should have subscribed to STARTED event"
|
assert len(subscriptions) > 0, "Should have subscribed to STARTED event"
|
||||||
assert any("UpdateChecker.attach" in name for name, _ in subscriptions), (
|
assert any("UpdateChecker.attach" in name for name, _ in subscriptions), (
|
||||||
|
|
@ -360,3 +381,119 @@ class TestUpdateChecker:
|
||||||
if checker and checker._job_id:
|
if checker and checker._job_id:
|
||||||
scheduler.remove(checker._job_id)
|
scheduler.remove(checker._job_id)
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("app.library.UpdateChecker.async_client")
|
||||||
|
async def test_check_ytdlp_version_finds_newer_version(self, mock_client):
|
||||||
|
"""Test that yt-dlp check detects when a newer version is available."""
|
||||||
|
from app.library.config import Config
|
||||||
|
from app.library.UpdateChecker import UpdateChecker
|
||||||
|
|
||||||
|
config = Config.get_instance()
|
||||||
|
config.check_for_updates = True
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"tag_name": "9999.12.31"}
|
||||||
|
|
||||||
|
mock_context = AsyncMock()
|
||||||
|
mock_context.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
||||||
|
mock_client.return_value = mock_context
|
||||||
|
|
||||||
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
|
status, new_version = await checker._check_ytdlp_version()
|
||||||
|
|
||||||
|
assert "update_available" == status, "Should return update_available status for yt-dlp"
|
||||||
|
assert "9999.12.31" == new_version, "Should return new yt-dlp version tag"
|
||||||
|
assert "9999.12.31" == config.yt_new_version, "Should store new yt-dlp version tag"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("app.library.UpdateChecker.async_client")
|
||||||
|
async def test_check_ytdlp_version_no_update_available(self, mock_client):
|
||||||
|
"""Test that yt-dlp check correctly handles when no update is available."""
|
||||||
|
from app.library.config import Config
|
||||||
|
from app.library.UpdateChecker import UpdateChecker
|
||||||
|
|
||||||
|
config = Config.get_instance()
|
||||||
|
config.check_for_updates = True
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"tag_name": "2020.01.01"}
|
||||||
|
|
||||||
|
mock_context = AsyncMock()
|
||||||
|
mock_context.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
||||||
|
mock_client.return_value = mock_context
|
||||||
|
|
||||||
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
|
status, new_version = await checker._check_ytdlp_version()
|
||||||
|
|
||||||
|
assert "up_to_date" == status, "Should return up_to_date status for yt-dlp"
|
||||||
|
assert new_version is None, "Should return None for yt-dlp new_version"
|
||||||
|
assert "" == config.yt_new_version, "Should clear yt_new_version when no update available"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("app.library.UpdateChecker.async_client")
|
||||||
|
async def test_check_ytdlp_version_handles_http_error(self, mock_client):
|
||||||
|
"""Test that yt-dlp check handles HTTP errors gracefully."""
|
||||||
|
from app.library.config import Config
|
||||||
|
from app.library.UpdateChecker import UpdateChecker
|
||||||
|
|
||||||
|
config = Config.get_instance()
|
||||||
|
config.check_for_updates = True
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 500
|
||||||
|
|
||||||
|
mock_context = AsyncMock()
|
||||||
|
mock_context.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
||||||
|
mock_client.return_value = mock_context
|
||||||
|
|
||||||
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
|
status, new_version = await checker._check_ytdlp_version()
|
||||||
|
|
||||||
|
assert "error" == status, "Should return error status for yt-dlp on HTTP error"
|
||||||
|
assert new_version is None, "Should return None for yt-dlp new_version on error"
|
||||||
|
assert "" == config.yt_new_version, "Should not set yt_new_version on HTTP error"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("app.library.UpdateChecker.async_client")
|
||||||
|
async def test_check_for_updates_caches_separately(self, mock_client):
|
||||||
|
"""Test that app and yt-dlp checks are cached separately."""
|
||||||
|
from app.library.config import Config
|
||||||
|
from app.library.UpdateChecker import UpdateChecker
|
||||||
|
|
||||||
|
config = Config.get_instance()
|
||||||
|
config.check_for_updates = True
|
||||||
|
config.new_version = ""
|
||||||
|
config.yt_new_version = ""
|
||||||
|
|
||||||
|
mock_app_response = MagicMock()
|
||||||
|
mock_app_response.status_code = 200
|
||||||
|
mock_app_response.json.return_value = {"tag_name": "v2.0.0"}
|
||||||
|
|
||||||
|
mock_ytdlp_response = MagicMock()
|
||||||
|
mock_ytdlp_response.status_code = 200
|
||||||
|
mock_ytdlp_response.json.return_value = {"tag_name": "2026.01.01"}
|
||||||
|
|
||||||
|
mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response])
|
||||||
|
mock_context = AsyncMock()
|
||||||
|
mock_context.__aenter__.return_value.get = mock_get
|
||||||
|
mock_client.return_value = mock_context
|
||||||
|
|
||||||
|
with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"):
|
||||||
|
checker = UpdateChecker.get_instance(config=config)
|
||||||
|
(app_status1, app_version1), (ytdlp_status1, ytdlp_version1) = await checker.check_for_updates()
|
||||||
|
|
||||||
|
assert "update_available" == app_status1, "Should find app update on first call"
|
||||||
|
assert "update_available" == ytdlp_status1, "Should find yt-dlp update on first call"
|
||||||
|
|
||||||
|
(app_status2, app_version2), (ytdlp_status2, ytdlp_version2) = await checker.check_for_updates()
|
||||||
|
|
||||||
|
assert "update_available" == app_status2, "Should return cached app result"
|
||||||
|
assert "update_available" == ytdlp_status2, "Should return cached yt-dlp result"
|
||||||
|
assert app_version1 == app_version2, "App versions should match from cache"
|
||||||
|
assert ytdlp_version1 == ytdlp_version2, "yt-dlp versions should match from cache"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="message is-warning" v-if="status === 'disconnected'">
|
<div class="message is-warning" v-if="status !== 'connected'">
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
<span class="icon"><i class="fas fa-info-circle" /></span>
|
<span class="icon"><i class="fas"
|
||||||
<span>
|
:class="{ 'fa-info-circle': status === 'disconnected', 'fa-spinner fa-pulse': status === 'connecting' }" /></span>
|
||||||
Websocket connection lost, <NuxtLink class="is-bold" @click.prevent="() => $emit('reconnect')">Click here</NuxtLink>
|
<span v-if="status === 'disconnected'">
|
||||||
to reconnect.
|
Websocket connection lost, <NuxtLink class="is-bold" @click.prevent="() => $emit('reconnect')">Click here
|
||||||
|
</NuxtLink> to reconnect.
|
||||||
|
</span>
|
||||||
|
<span v-else-if="status === 'connecting'">
|
||||||
|
Connecting to websocket server...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -182,31 +182,29 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="config.app?.check_for_updates">
|
<p v-if="config.app?.new_version" class="is-size-7 mb-2" style="opacity: 0.8">
|
||||||
<p v-if="config.app?.new_version" class="is-size-7 mb-2" style="opacity: 0.8">
|
<span class="icon-text">
|
||||||
<span class="icon-text">
|
<span class="icon has-text-warning"><i class="fas fa-circle-info" /></span>
|
||||||
<span class="icon has-text-warning"><i class="fas fa-circle-info" /></span>
|
<span>Update available:</span>
|
||||||
<span>Update available:</span>
|
<NuxtLink :href="`https://github.com/ArabCoders/ytptube/releases/tag/${config.app.new_version}`"
|
||||||
<NuxtLink :href="`https://github.com/ArabCoders/ytptube/releases/tag/${config.app.new_version}`"
|
target="_blank" class="has-text-weight-semibold ml-1">
|
||||||
target="_blank" class="has-text-weight-semibold ml-1">
|
{{ config.app.new_version }}
|
||||||
{{ config.app.new_version }}
|
</NuxtLink>
|
||||||
</NuxtLink>
|
</span>
|
||||||
</span>
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-else class="is-size-7 mb-2" style="opacity: 0.6">
|
<p v-else class="is-size-7 mb-2" style="opacity: 0.6">
|
||||||
<button @click="checkForUpdates" :disabled="checkingUpdates" class="is-text is-small p-0 is-size-7"
|
<button @click="checkForUpdates" :disabled="checkingUpdates" class="is-text is-small p-0 is-size-7"
|
||||||
:class="{ 'is-loading': checkingUpdates }"
|
:class="{ 'is-loading': checkingUpdates }"
|
||||||
style="opacity: 0.8; height: auto; vertical-align: baseline; border: none; text-decoration: none; background: none;">
|
style="opacity: 0.8; height: auto; vertical-align: baseline; border: none; text-decoration: none; background: none;">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas" :class="checkingUpdates ? 'fa-spinner fa-spin' : 'fa-circle-check'" />
|
<i class="fas" :class="checkingUpdates ? 'fa-spinner fa-spin' : 'fa-circle-check'" />
|
||||||
</span>
|
|
||||||
<span>{{ updateCheckMessage }}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
<span>{{ updateCheckMessage }}</span>
|
||||||
</p>
|
</span>
|
||||||
</template>
|
</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p v-if="config.app?.started" class="is-size-7 mb-0" style="opacity: 0.6">
|
<p v-if="config.app?.started" class="is-size-7 mb-0" style="opacity: 0.6">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
|
|
@ -230,6 +228,19 @@
|
||||||
</span>
|
</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<span class="is-size-7 ml-1" style="opacity: 0.6">{{ config?.app?.ytdlp_version || 'unknown' }}</span>
|
<span class="is-size-7 ml-1" style="opacity: 0.6">{{ config?.app?.ytdlp_version || 'unknown' }}</span>
|
||||||
|
|
||||||
|
<p v-if="config.app?.yt_new_version" class="is-size-7 mb-0 mt-2" style="opacity: 0.8">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="has-tooltip" v-tooltip="`Restart container to update yt-dlp`">
|
||||||
|
<span class="icon has-text-warning"><i class="fas fa-circle-info" /></span>
|
||||||
|
<span>Update available:</span>
|
||||||
|
</span>
|
||||||
|
<NuxtLink :href="`https://github.com/yt-dlp/yt-dlp/releases/tag/${config.app.yt_new_version}`"
|
||||||
|
target="_blank" class="has-text-weight-semibold ml-1">
|
||||||
|
{{ config.app.yt_new_version }}
|
||||||
|
</NuxtLink>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -289,6 +300,7 @@ import Dialog from '~/components/Dialog.vue'
|
||||||
import Simple from '~/components/Simple.vue'
|
import Simple from '~/components/Simple.vue'
|
||||||
import Shutdown from '~/components/shutdown.vue'
|
import Shutdown from '~/components/shutdown.vue'
|
||||||
import Markdown from '~/components/Markdown.vue'
|
import Markdown from '~/components/Markdown.vue'
|
||||||
|
import type { version_check } from '~/types'
|
||||||
|
|
||||||
const selectedTheme = useStorage('theme', 'auto')
|
const selectedTheme = useStorage('theme', 'auto')
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
|
|
@ -326,22 +338,30 @@ const checkForUpdates = async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await parse_api_response<version_check>(await response.json())
|
||||||
|
|
||||||
switch (data.status) {
|
if ('update_available' === data.app.status) {
|
||||||
case 'update_available':
|
config.app.new_version = data.app.new_version
|
||||||
updateCheckMessage.value = 'Update found!'
|
|
||||||
config.app.new_version = data.new_version
|
|
||||||
break
|
|
||||||
case 'up_to_date':
|
|
||||||
updateCheckMessage.value = 'Up to date ✓'
|
|
||||||
setTimeout(() => updateCheckMessage.value = msg, 3000)
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
updateCheckMessage.value = 'Check failed'
|
|
||||||
setTimeout(() => updateCheckMessage.value = msg, 3000)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
if (data.ytdlp && 'update_available' === data.ytdlp.status) {
|
||||||
|
config.app.yt_new_version = data.ytdlp.new_version
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show "Update found!" if app has update (button is in app section)
|
||||||
|
if ('update_available' === data.app.status) {
|
||||||
|
updateCheckMessage.value = 'Update found!'
|
||||||
|
} else if ('up_to_date' === data.app.status && 'up_to_date' === data.ytdlp?.status) {
|
||||||
|
updateCheckMessage.value = 'Up to date ✓'
|
||||||
|
setTimeout(() => updateCheckMessage.value = msg, 3000)
|
||||||
|
} else if ('up_to_date' === data.app.status && 'update_available' === data.ytdlp?.status) {
|
||||||
|
// App is up to date, but yt-dlp has update (shows in yt-dlp section)
|
||||||
|
updateCheckMessage.value = 'Up to date ✓'
|
||||||
|
setTimeout(() => updateCheckMessage.value = msg, 3000)
|
||||||
|
} else {
|
||||||
|
updateCheckMessage.value = 'Check failed'
|
||||||
|
setTimeout(() => updateCheckMessage.value = msg, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Update check failed:', e)
|
console.error('Update check failed:', e)
|
||||||
updateCheckMessage.value = 'Check failed'
|
updateCheckMessage.value = 'Check failed'
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||||
default_pagination: 50,
|
default_pagination: 50,
|
||||||
check_for_updates: true,
|
check_for_updates: true,
|
||||||
new_version: '',
|
new_version: '',
|
||||||
|
yt_new_version: '',
|
||||||
},
|
},
|
||||||
presets: [
|
presets: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
2
ui/app/types/config.d.ts
vendored
2
ui/app/types/config.d.ts
vendored
|
|
@ -49,6 +49,8 @@ type AppConfig = {
|
||||||
check_for_updates: boolean
|
check_for_updates: boolean
|
||||||
/** New version available, empty string if none */
|
/** New version available, empty string if none */
|
||||||
new_version: string
|
new_version: string
|
||||||
|
/** New yt-dlp version available, empty string if none */
|
||||||
|
yt_new_version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigState = {
|
type ConfigState = {
|
||||||
|
|
|
||||||
15
ui/app/types/index.d.ts
vendored
15
ui/app/types/index.d.ts
vendored
|
|
@ -3,4 +3,17 @@ type ImportedItem = {
|
||||||
_version: string
|
_version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ImportedItem }
|
type version_check = {
|
||||||
|
app: {
|
||||||
|
status: 'up_to_date' | 'update_available' | 'error',
|
||||||
|
current_version: string,
|
||||||
|
new_version: string
|
||||||
|
},
|
||||||
|
ytdlp: {
|
||||||
|
status: 'up_to_date' | 'update_available' | 'error',
|
||||||
|
current_version: string,
|
||||||
|
new_version: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ImportedItem, version_check }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue