Skip to content

Commit

Permalink
Merge branch 'main' into filename_thumbs
Browse files Browse the repository at this point in the history
  • Loading branch information
CyanVoxel authored Dec 13, 2024
2 parents c54599a + dec9f1d commit 15567a1
Show file tree
Hide file tree
Showing 12 changed files with 387 additions and 147 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on: [ push, pull_request ]
jobs:
pytest:
name: Run tests
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04

steps:
- name: Checkout repo
Expand All @@ -20,11 +20,12 @@ jobs:
- name: Install system dependencies
run: |
# dont run update, it is slow
# sudo apt-get update
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libxkbcommon-x11-0 \
x11-utils \
libyaml-dev \
libgl1 \
libegl1 \
libxcb-icccm4 \
libxcb-image0 \
Expand Down
Binary file added docs/assets/ffmpeg_windows_download.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 21 additions & 8 deletions docs/help/ffmpeg.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,51 @@
FFmpeg is required for thumbnail previews and playback features on audio and video files. FFmpeg is a free Open Source project dedicated to the handling of multimedia (video, audio, etc) files. For more information, see their official website at [ffmpeg.org](https://www.ffmpeg.org/).

## Installation on Windows

### Prebuilt Binaries
Pre-built binaries from trusted sources are available on the [FFmpeg website](https://www.ffmpeg.org/download.html#build-windows). To install:

Pre-built binaries from trusted sources are available on the [FFmpeg website](https://www.ffmpeg.org/download.html). Under "More downloading options" click on the Windows section, then under "Windows EXE Files" select a source to download a build from. Follow any further download instructions from whichever build website you choose.

![Windows Download Location](../assets/ffmpeg_windows_download.png)

!!! note
Do NOT download the source code by mistake!

To Install:

1. Download 7z or zip file and extract it (right click > Extract All)
2. Move extracted contents to a unique folder (i.e; `c:\ffmpeg` or `c:\Program Files\ffmpeg`)
3. Add FFmpeg to your PATH
3. Add FFmpeg to your system PATH

1. Go to "Edit the system environment variables"
2. Under "User Variables", select "Path" then edit
3. Click new and add `<Your folder>\bin` (e.g; `c:\ffmpeg\bin` or `c:\Program Files\ffmpeg\bin`)
4. Click okay
1. In Windows, search for or go to "Edit the system environment variables" under the Control Panel
2. Under "User Variables", select "Path" then edit
3. Click new and add `<Your folder>\bin` (e.g; `c:\ffmpeg\bin` or `c:\Program Files\ffmpeg\bin`)
4. Click "Okay"

### Package Managers

FFmpeg is also available from:

1. WinGet (`winget install ffmpeg`)
2. Scoop (`scoop install main/ffmpeg`)
3. Chocolatey (`choco install ffmpeg-full`)

## Installation on Mac

### Homebrew
FFmpeg is available via [Homebrew](https://brew.sh/) and can be installed via:

`brew install ffmpeg`
FFmpeg is available under the macOS section of the [FFmpeg website](https://www.ffmpeg.org/download.html) or can be installed via [Homebrew](https://brew.sh/) using `brew install ffmpeg`.

## Installation on Linux

### Package Managers

FFmpeg may be installed by default on some Linux distributions, but if not, it is available via your distro's package manager of choice:

1. Debian/Ubuntu (`sudo apt install ffmpeg`)
2. Fedora (`sudo dnf install ffmpeg-free`)
3. Arch (`sudo pacman -S ffmpeg`)

# Help

For additional help, please join the [Discord](https://discord.gg/hRNnVKhF2G) or create an Issue on the [GitHub repository](https://github.com/TagStudioDev/TagStudio)
31 changes: 8 additions & 23 deletions tagstudio/src/qt/modals/delete_unlinked.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import typing

from PySide6.QtCore import Qt, QThreadPool, Signal
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QHBoxLayout,
Expand All @@ -15,8 +15,6 @@
QWidget,
)
from src.core.utils.missing_files import MissingRegistry
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.widgets.progress import ProgressWidget

# Only import for type checking/autocompletion, will not be imported at runtime.
Expand Down Expand Up @@ -77,33 +75,20 @@ def refresh_list(self):

self.model.clear()
for i in self.tracker.missing_files:
self.model.appendRow(QStandardItem(str(i.path)))
item = QStandardItem(str(i.path))
item.setEditable(False)
self.model.appendRow(item)

def delete_entries(self):
def displayed_text(x):
return f"Deleting {x}/{self.tracker.missing_files_count} Unlinked Entries"

pw = ProgressWidget(
window_title="Deleting Entries",
label_text="",
cancel_button_text=None,
minimum=0,
maximum=self.tracker.missing_files_count,
)
pw.show()

iterator = FunctionIterator(self.tracker.execute_deletion)
files_count = self.tracker.missing_files_count
iterator.value.connect(
lambda idx: (
pw.update_progress(idx),
pw.update_label(f"Deleting {idx}/{files_count} Unlinked Entries"),
)
)

r = CustomRunnable(iterator.run)
QThreadPool.globalInstance().start(r)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.done.emit(),
)
)
pw.from_iterable_function(self.tracker.execute_deletion, displayed_text, self.done.emit)
229 changes: 229 additions & 0 deletions tagstudio/src/qt/modals/drop_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio

import enum
import shutil
from pathlib import Path
from typing import TYPE_CHECKING

import structlog
from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QListView,
QPushButton,
QVBoxLayout,
QWidget,
)
from src.qt.widgets.progress import ProgressWidget

if TYPE_CHECKING:
from src.qt.ts_qt import QtDriver

logger = structlog.get_logger(__name__)


class DuplicateChoice(enum.StrEnum):
SKIP = "Skipped"
OVERWRITE = "Overwritten"
RENAME = "Renamed"
CANCEL = "Cancelled"


class DropImportModal(QWidget):
DUPE_NAME_LIMT: int = 5

def __init__(self, driver: "QtDriver"):
super().__init__()

self.driver: QtDriver = driver

# Widget ======================
self.setWindowTitle("Conflicting File(s)")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)

self.desc_widget = QLabel()
self.desc_widget.setObjectName("descriptionLabel")
self.desc_widget.setWordWrap(True)
self.desc_widget.setText("The following files have filenames already exist in the library")
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)

# Duplicate File List ========
self.list_view = QListView()
self.model = QStandardItemModel()
self.list_view.setModel(self.model)

# Buttons ====================
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)

self.skip_button = QPushButton()
self.skip_button.setText("&Skip")
self.skip_button.setDefault(True)
self.skip_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.SKIP))
self.button_layout.addWidget(self.skip_button)

self.overwrite_button = QPushButton()
self.overwrite_button.setText("&Overwrite")
self.overwrite_button.clicked.connect(
lambda: self.begin_transfer(DuplicateChoice.OVERWRITE)
)
self.button_layout.addWidget(self.overwrite_button)

self.rename_button = QPushButton()
self.rename_button.setText("&Rename")
self.rename_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.RENAME))
self.button_layout.addWidget(self.rename_button)

self.cancel_button = QPushButton()
self.cancel_button.setText("&Cancel")
self.cancel_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.CANCEL))
self.button_layout.addWidget(self.cancel_button)

# Layout =====================
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.list_view)
self.root_layout.addWidget(self.button_container)

def import_urls(self, urls: list[QUrl]):
"""Add a colleciton of urls to the library."""
self.files: list[Path] = []
self.dirs_in_root: list[Path] = []
self.duplicate_files: list[Path] = []

self.collect_files_to_import(urls)

if len(self.duplicate_files) > 0:
self.ask_duplicates_choice()
else:
self.begin_transfer()

def collect_files_to_import(self, urls: list[QUrl]):
"""Collect one or more files from drop event urls."""
for url in urls:
if not url.isLocalFile():
continue

file = Path(url.toLocalFile())

if file.is_dir():
for f in file.glob("**/*"):
if f.is_dir():
continue

self.files.append(f)
if (self.driver.lib.library_dir / self._get_relative_path(file)).exists():
self.duplicate_files.append(f)

self.dirs_in_root.append(file.parent)
else:
self.files.append(file)

if file.parent not in self.dirs_in_root:
self.dirs_in_root.append(
file.parent
) # to create relative path of files not in folder

if (Path(self.driver.lib.library_dir) / file.name).exists():
self.duplicate_files.append(file)

def ask_duplicates_choice(self):
"""Display the message widgeth with a list of the duplicated files."""
self.desc_widget.setText(
f"The following {len(self.duplicate_files)} file(s) have filenames already exist in the library." # noqa: E501
)

self.model.clear()
for dupe in self.duplicate_files:
item = QStandardItem(str(self._get_relative_path(dupe)))
item.setEditable(False)
self.model.appendRow(item)

self.driver.main_window.raise_()
self.show()

def begin_transfer(self, choice: DuplicateChoice | None = None):
"""Display a progress bar and begin copying files into library."""
self.hide()
self.choice: DuplicateChoice | None = choice
logger.info("duplicated choice selected", choice=self.choice)
if self.choice == DuplicateChoice.CANCEL:
return

def displayed_text(x):
text = (
f"Importing New Files...\n{x[0] + 1} File{'s' if x[0] + 1 != 1 else ''} Imported."
)
if self.choice:
text += f" {x[1]} {self.choice.value}"

return text

pw = ProgressWidget(
window_title="Import Files",
label_text="Importing New Files...",
cancel_button_text=None,
minimum=0,
maximum=len(self.files),
)

pw.from_iterable_function(
self.copy_files,
displayed_text,
self.driver.add_new_files_callback,
self.deleteLater,
)

def copy_files(self):
"""Copy files from original location to the library directory."""
file_count = 0
duplicated_files_progress = 0
for file in self.files:
if file.is_dir():
continue

dest_file = self._get_relative_path(file)

if file in self.duplicate_files:
duplicated_files_progress += 1
if self.choice == DuplicateChoice.SKIP:
file_count += 1
continue
elif self.choice == DuplicateChoice.RENAME:
new_name = self._get_renamed_duplicate_filename(dest_file)
dest_file = dest_file.with_name(new_name)

(self.driver.lib.library_dir / dest_file).parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(file, self.driver.lib.library_dir / dest_file)

file_count += 1
yield [file_count, duplicated_files_progress]

def _get_relative_path(self, path: Path) -> Path:
for dir in self.dirs_in_root:
if path.is_relative_to(dir):
return path.relative_to(dir)
return Path(path.name)

def _get_renamed_duplicate_filename(self, filepath: Path) -> str:
index = 2
o_filename = filepath.name

try:
dot_idx = o_filename.index(".")
except ValueError:
dot_idx = len(o_filename)

while (self.driver.lib.library_dir / filepath).exists():
filepath = filepath.with_name(
o_filename[:dot_idx] + f" ({index})" + o_filename[dot_idx:]
)
index += 1
return filepath.name
Loading

0 comments on commit 15567a1

Please sign in to comment.