Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies = [
"typing_extensions~=4.13",
"ujson~=5.10",
"wcmatch==10.*",
"requests~=2.31.0",
]

[project.optional-dependencies]
Expand Down
33 changes: 30 additions & 3 deletions src/tagstudio/core/ts_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
"""The core classes and methods of TagStudio."""

import json
import re
from pathlib import Path

import requests
import structlog

from tagstudio.core.constants import TS_FOLDER_NAME
from tagstudio.core.library.alchemy.fields import FieldID
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.types import unwrap

logger = structlog.get_logger(__name__)

MOST_RECENT_RELEASE_VERSION: str | None = None


class TagStudioCore:
def __init__(self):
Expand Down Expand Up @@ -101,11 +106,11 @@ def match_conditions(cls, lib: Library, entry_id: int) -> bool:
"""Match defined conditions against a file to add Entry data."""
# TODO - what even is this file format?
# TODO: Make this stored somewhere better instead of temporarily in this JSON file.
cond_file = lib.library_dir / TS_FOLDER_NAME / "conditions.json"
cond_file = unwrap(lib.library_dir) / TS_FOLDER_NAME / "conditions.json"
if not cond_file.is_file():
return False

entry: Entry = lib.get_entry(entry_id)
entry: Entry = unwrap(lib.get_entry(entry_id))

try:
with open(cond_file, encoding="utf8") as f:
Expand All @@ -130,7 +135,9 @@ def match_conditions(cls, lib: Library, entry_id: int) -> bool:
is_new = field["id"] not in entry_field_types
field_key = field["id"]
if is_new:
lib.add_field_to_entry(entry.id, field_key, field["value"])
lib.add_field_to_entry(
entry.id, field_id=field_key, value=field["value"]
)
else:
lib.update_entry_field(entry.id, field_key, field["value"])

Expand Down Expand Up @@ -181,3 +188,23 @@ def _build_instagram_url(cls, entry: Entry):
except Exception:
logger.exception("Error building Instagram URL.", entry=entry)
return ""

@staticmethod
def get_most_recent_release_version() -> str | None:
"""Get the version of the most recent Github release."""
global MOST_RECENT_RELEASE_VERSION
if MOST_RECENT_RELEASE_VERSION is not None:
return MOST_RECENT_RELEASE_VERSION

resp = requests.get("https://api.github.com/repos/TagStudioDev/TagStudio/releases/latest")
assert resp.status_code == 200, "Could not fetch information on latest release."

data = resp.json()
tag: str = data["tag_name"]
assert tag.startswith("v")

version = tag[1:]
assert re.match(r"^\d+\.\d+\.\d+(-\w+)?$", version) is not None, "Invalid version format."

MOST_RECENT_RELEASE_VERSION = version
return version
26 changes: 26 additions & 0 deletions src/tagstudio/core/utils/str_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

import re

from tagstudio.core.utils.types import unwrap


def strip_punctuation(string: str) -> str:
"""Returns a given string stripped of all punctuation characters."""
Expand Down Expand Up @@ -32,3 +36,25 @@ def strip_web_protocol(string: str) -> str:
for prefix in prefixes:
string = string.removeprefix(prefix)
return string


def is_version_outdated(current, latest) -> bool:
regex = re.compile(r"^(\d+)\.(\d+)\.(\d+)(-\w+)?$")
mcurrent = unwrap(regex.match(current))
mlatest = unwrap(regex.match(latest))

return (
int(mlatest[1]) > int(mcurrent[1])
or (mlatest[1] == mcurrent[1] and int(mlatest[2]) > int(mcurrent[2]))
or (
mlatest[1] == mcurrent[1]
and mlatest[2] == mcurrent[2]
and int(mlatest[3]) > int(mcurrent[3])
)
or (
mlatest[1] == mcurrent[1]
and mlatest[2] == mcurrent[2]
and mlatest[3] == mcurrent[3]
and (mlatest[4] is None and mcurrent[4] is not None)
)
)
39 changes: 39 additions & 0 deletions src/tagstudio/qt/controllers/out_of_date_message_box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import structlog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QMessageBox

from tagstudio.core.constants import VERSION
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.translations import Translations

logger = structlog.get_logger(__name__)


class OutOfDateMessageBox(QMessageBox):
"""A warning dialog for if the TagStudio is not running under the latest release version."""

def __init__(self):
super().__init__()

title = Translations.format("version_modal.title")
self.setWindowTitle(title)
self.setIcon(QMessageBox.Icon.Warning)
self.setWindowModality(Qt.WindowModality.ApplicationModal)

self.setStandardButtons(
QMessageBox.StandardButton.Ignore | QMessageBox.StandardButton.Cancel
)
self.setDefaultButton(QMessageBox.StandardButton.Ignore)
# Enables the cancel button but hides it to allow for click X to close dialog
self.button(QMessageBox.StandardButton.Cancel).hide()

red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
latest_release_version = TagStudioCore.get_most_recent_release_version()
status = Translations.format(
"version_modal.status",
installed_version=f"<span style='color:{red}'>{VERSION}</span>",
latest_release_version=f"<span style='color:{green}'>{latest_release_version}</span>",
)
self.setText(f"{Translations['version_modal.description']}<br><br>{status}")
14 changes: 14 additions & 0 deletions src/tagstudio/qt/mixed/about_modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from tagstudio.core.constants import VERSION, VERSION_BRANCH
from tagstudio.core.enums import Theme
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.previews.vendored import ffmpeg
from tagstudio.qt.resource_manager import ResourceManager
Expand Down Expand Up @@ -103,6 +104,19 @@ def __init__(self, config_path):
self.system_info_layout = QFormLayout(self.system_info_widget)
self.system_info_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)

# Version
version_title = QLabel("Version")
most_recent_release = TagStudioCore.get_most_recent_release_version()
version_content_style = self.form_content_style
if most_recent_release == VERSION:
version_content = QLabel(f"{VERSION}")
else:
version_content = QLabel(f"{VERSION} (Latest Release: {most_recent_release})")
version_content_style += "color: #d9534f;"
version_content.setStyleSheet(version_content_style)
version_content.setMaximumWidth(version_content.sizeHint().width())
self.system_info_layout.addRow(version_title, version_content)

# License
license_title = QLabel(f"{Translations['about.license']}")
license_content = QLabel("GPLv3")
Expand Down
6 changes: 5 additions & 1 deletion src/tagstudio/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
from tagstudio.core.media_types import MediaCategories
from tagstudio.core.query_lang.util import ParsingError
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.str_formatting import strip_web_protocol
from tagstudio.core.utils.str_formatting import is_version_outdated, strip_web_protocol
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.cache_manager import CacheManager
from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox
Expand All @@ -71,6 +71,7 @@
from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal
from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal
from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow
from tagstudio.qt.controllers.out_of_date_message_box import OutOfDateMessageBox
from tagstudio.qt.global_settings import (
DEFAULT_GLOBAL_SETTINGS_PATH,
GlobalSettings,
Expand Down Expand Up @@ -590,6 +591,9 @@ def create_about_modal():
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
FfmpegMissingMessageBox().show()

if is_version_outdated(VERSION, TagStudioCore.get_most_recent_release_version()):
OutOfDateMessageBox().exec()

self.app.exec()
self.shutdown()

Expand Down
3 changes: 3 additions & 0 deletions src/tagstudio/resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@
"trash.dialog.title.singular": "Delete File",
"trash.name.generic": "Trash",
"trash.name.windows": "Recycle Bin",
"version_modal.title": "TagStudio Update Available",
"version_modal.description": "A new version of TagStudio is available! You can download the latest release from <a href=\"https://github.com/TagStudioDev/TagStudio\">Github</a>.",
"version_modal.status": "Installed Version: {installed_version}<br>Latest Release Version: {latest_release_version}",
"view.size.0": "Mini",
"view.size.1": "Small",
"view.size.2": "Medium",
Expand Down