├── cozy ├── view.py ├── db │ ├── __init__.py │ ├── storage_blacklist.py │ ├── file.py │ ├── artwork_cache.py │ ├── storage.py │ ├── offline_cache.py │ ├── track.py │ ├── track_to_file.py │ ├── collation.py │ ├── settings.py │ ├── book.py │ └── model_base.py ├── media │ ├── __init__.py │ ├── chapter.py │ ├── media_file.py │ └── media_detector.py ├── model │ ├── __init__.py │ ├── storage.py │ ├── chapter.py │ └── settings.py ├── report │ ├── __init__.py │ ├── log_level.py │ ├── reporter.py │ └── report_to_loki.py ├── ui │ ├── __init__.py │ ├── widgets │ │ ├── __init__.py │ │ ├── progress_popover.py │ │ ├── search_results.py │ │ ├── filter_list_box.py │ │ ├── playback_speed_popover.py │ │ ├── book_row.py │ │ ├── error_reporting.py │ │ ├── welcome_dialog.py │ │ └── seek_bar.py │ ├── list_box_separator_row.py │ ├── toaster.py │ ├── list_box_row_with_data.py │ ├── delete_book_view.py │ ├── db_migration_failed_view.py │ ├── chapter_element.py │ ├── import_failed_dialog.py │ ├── about_window.py │ ├── preferences_window.py │ ├── file_not_found_dialog.py │ └── app_view.py ├── control │ ├── __init__.py │ ├── application_directories.py │ └── time_format.py ├── view_model │ ├── __init__.py │ ├── app_view_model.py │ ├── settings_view_model.py │ ├── playback_speed_view_model.py │ └── search_view_model.py ├── architecture │ ├── __init__.py │ ├── singleton.py │ ├── profiler.py │ ├── event_sender.py │ └── observable.py ├── __init__.py ├── enums.py ├── tools.py └── application.py ├── test ├── __init__.py ├── cozy │ ├── __init__.py │ ├── media │ │ ├── __init__.py │ │ └── test_files.py │ ├── model │ │ ├── __init__.py │ │ ├── storage_block_list.py │ │ ├── test_split_strings_to_set.py │ │ ├── test_storage.py │ │ └── test_library.py │ └── mocks.py ├── storages.json └── books.json ├── .github ├── FUNDING.yml ├── issue_template.md ├── dependabot.yml └── workflows │ ├── checks.yml │ ├── tests.yml │ ├── flatpak.yml │ ├── flathub.yml │ ├── opensuse.yml │ └── aur.yml ├── po ├── extra │ ├── POTFILES │ ├── meson.build │ ├── LINGUAS │ ├── extra.pot │ ├── fa.po │ ├── hi.po │ ├── nl.po │ ├── he.po │ ├── hu.po │ ├── fa_IR.po │ ├── cs.po │ ├── hr.po │ ├── sv.po │ ├── fi.po │ └── it.po ├── meson.build ├── LINGUAS └── POTFILES ├── requirements.txt ├── data ├── icons │ ├── meson.build │ └── hicolor │ │ └── scalable │ │ └── actions │ │ ├── list-add-symbolic.svg │ │ ├── cozy.folder-symbolic.svg │ │ ├── cozy.default-storage-symbolic.svg │ │ ├── cozy.volume-low-symbolic.svg │ │ ├── cozy.rewind-symbolic.svg │ │ ├── cozy.forward-symbolic.svg │ │ ├── cozy.author-symbolic.svg │ │ ├── cozy.volume-muted-symbolic.svg │ │ ├── cozy.recent-symbolic.svg │ │ ├── cozy.timer-symbolic.svg │ │ ├── cozy.book-open-symbolic.svg │ │ ├── cozy.volume-high-symbolic.svg │ │ ├── cozy.volume-medium-symbolic.svg │ │ ├── cozy.storage-symbolic.svg │ │ ├── cozy.bed-symbolic.svg │ │ ├── cozy.settings-symbolic.svg │ │ ├── cozy.reader-symbolic.svg │ │ ├── cozy.no-bed-symbolic.svg │ │ ├── cozy.search-large-symbolic.svg │ │ ├── cozy.playback-speed-symbolic.svg │ │ ├── cozy.network-folder-symbolic.svg │ │ ├── cozy.library-symbolic.svg │ │ └── cozy.feedback-symbolic.svg ├── com.github.geigi.cozy.desktop ├── ui │ ├── progress_popover.blp │ ├── storage_row.blp │ ├── meson.build │ ├── playback_speed_popover.blp │ ├── chapter_element.blp │ ├── storage_locations.blp │ ├── error_reporting.blp │ ├── seek_bar.blp │ ├── search_page.blp │ ├── preferences.blp │ ├── headerbar.blp │ ├── welcome_dialog.blp │ ├── sleep_timer_dialog.blp │ └── book_card.blp ├── gresource.xml ├── style.css └── meson.build ├── pyproject.toml ├── .gitignore ├── .ci ├── obs_wait_for_build.sh └── flathub_wait_for_build.sh ├── flatpak └── com.github.geigi.cozy.json ├── cozy.doap ├── AUTHORS.md └── meson.build /cozy/view.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/media/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/report/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cozy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/control/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/ui/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/view_model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cozy/media/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cozy/model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/architecture/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cozy/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "@VERSION@" 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: geigi 4 | -------------------------------------------------------------------------------- /po/extra/POTFILES: -------------------------------------------------------------------------------- 1 | data/com.github.geigi.cozy.desktop 2 | data/com.github.geigi.cozy.appdata.xml -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | distro 2 | mutagen>=1.47 3 | peewee>=3.9.6 4 | pytz 5 | requests 6 | inject 7 | Pillow -------------------------------------------------------------------------------- /test/cozy/mocks.py: -------------------------------------------------------------------------------- 1 | class ApplicationSettingsMock: 2 | @property 3 | def swap_author_reader(self): 4 | return False 5 | -------------------------------------------------------------------------------- /po/extra/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('extra', 2 | args: [ 3 | '--from-code=UTF-8' 4 | ], 5 | preset: 'glib', 6 | install: false 7 | ) -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | icon_themes = ['hicolor'] 2 | foreach theme : icon_themes 3 | install_subdir(theme, install_dir: 'share/icons/') 4 | endforeach -------------------------------------------------------------------------------- /cozy/report/log_level.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class LogLevel(Enum): 5 | DEBUG = 1 6 | INFO = 2 7 | WARNING = 3 8 | ERROR = 4 -------------------------------------------------------------------------------- /cozy/db/storage_blacklist.py: -------------------------------------------------------------------------------- 1 | from peewee import CharField 2 | 3 | from cozy.db.model_base import ModelBase 4 | 5 | 6 | class StorageBlackList(ModelBase): 7 | path = CharField() 8 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Bug/Feature description 2 | 3 | ### Steps to reproduce 4 | 5 | ### System Information 6 | - Operating System: 7 | - Installation source: 8 | - Version of cozy: 9 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext(meson.project_name(), 2 | args: [ 3 | '--directory=' + meson.current_source_dir(), 4 | '--from-code=UTF-8' 5 | ], 6 | preset: 'glib' 7 | ) 8 | 9 | subdir('extra') -------------------------------------------------------------------------------- /test/storages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "/media/test" 4 | }, 5 | { 6 | "path": "/mnt/test", 7 | "location_type": 1, 8 | "default": true, 9 | "external": true 10 | } 11 | ] -------------------------------------------------------------------------------- /po/extra/LINGUAS: -------------------------------------------------------------------------------- 1 | be_BY 2 | cs 3 | de 4 | es 5 | fa_IR 6 | fi 7 | fr 8 | gl 9 | he 10 | hi 11 | hr 12 | hu 13 | it 14 | ms_MY 15 | nl 16 | pl 17 | pt 18 | pt_BR 19 | ru 20 | sv 21 | te 22 | tr 23 | uk 24 | -------------------------------------------------------------------------------- /cozy/db/file.py: -------------------------------------------------------------------------------- 1 | from peewee import CharField, IntegerField 2 | 3 | from cozy.db.model_base import ModelBase 4 | 5 | 6 | class File(ModelBase): 7 | path = CharField(unique=True) 8 | modified = IntegerField() 9 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | be_BY 2 | bg 3 | cs 4 | da 5 | de 6 | el 7 | eo 8 | es 9 | fa_IR 10 | fi 11 | fr 12 | gl 13 | he 14 | hi 15 | hr 16 | hu 17 | it 18 | ms_MY 19 | nl 20 | no 21 | oc 22 | pl 23 | pt 24 | pt_BR 25 | ru 26 | sv 27 | tr 28 | uk 29 | zh -------------------------------------------------------------------------------- /cozy/db/artwork_cache.py: -------------------------------------------------------------------------------- 1 | from peewee import CharField, ForeignKeyField 2 | 3 | from cozy.db.book import Book 4 | from cozy.db.model_base import ModelBase 5 | 6 | 7 | class ArtworkCache(ModelBase): 8 | book = ForeignKeyField(Book) 9 | uuid = CharField() 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /cozy/architecture/singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton(type): 2 | _instances = {} 3 | def __call__(cls, *args, **kwargs): 4 | if cls not in cls._instances: 5 | cls._instances[cls] = super().__call__(*args, **kwargs) 6 | return cls._instances[cls] 7 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/list-add-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /cozy/db/storage.py: -------------------------------------------------------------------------------- 1 | from peewee import BooleanField, CharField, IntegerField 2 | 3 | from cozy.db.model_base import ModelBase 4 | 5 | 6 | class Storage(ModelBase): 7 | path = CharField() 8 | location_type = IntegerField(default=0) 9 | default = BooleanField(default=False) 10 | external = BooleanField(default=False) 11 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | ruff: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v5 14 | - uses: chartboost/ruff-action@v1 15 | with: 16 | args: 'check --output-format github' 17 | -------------------------------------------------------------------------------- /cozy/db/offline_cache.py: -------------------------------------------------------------------------------- 1 | from peewee import BooleanField, CharField, ForeignKeyField 2 | 3 | from cozy.db.file import File 4 | from cozy.db.model_base import ModelBase 5 | 6 | 7 | class OfflineCache(ModelBase): 8 | original_file = ForeignKeyField(File, unique=True) 9 | copied = BooleanField(default=False) 10 | cached_file = CharField() 11 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | - "master" 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ghcr.io/geigi/cozy-ci:main 15 | 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - name: Run pytest 20 | run: pytest 21 | -------------------------------------------------------------------------------- /cozy/db/track.py: -------------------------------------------------------------------------------- 1 | from peewee import CharField, FloatField, ForeignKeyField, IntegerField 2 | 3 | from cozy.db.book import Book 4 | from cozy.db.model_base import ModelBase 5 | 6 | 7 | class Track(ModelBase): 8 | name = CharField() 9 | number = IntegerField() 10 | disk = IntegerField() 11 | position = IntegerField() 12 | book = ForeignKeyField(Book) 13 | length = FloatField() 14 | -------------------------------------------------------------------------------- /cozy/db/track_to_file.py: -------------------------------------------------------------------------------- 1 | from peewee import ForeignKeyField, IntegerField 2 | 3 | from cozy.db.file import File 4 | from cozy.db.model_base import ModelBase 5 | from cozy.db.track import Track 6 | 7 | 8 | class TrackToFile(ModelBase): 9 | track = ForeignKeyField(Track, unique=True, backref='track_to_file') 10 | file = ForeignKeyField(File, unique=False) 11 | start_at = IntegerField() 12 | -------------------------------------------------------------------------------- /cozy/media/chapter.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass 4 | class Chapter: 5 | name: str | None 6 | position: int | None # in nanoseconds 7 | length: float | None # in seconds... sigh # FIXME: finally use nanoseconds everywhere consistently 8 | number: int | None 9 | 10 | def is_valid(self): 11 | return self.name is not None and self.position is not None 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | skip-magic-trailing-comma = true 4 | 5 | [tool.isort] 6 | line_length = 100 7 | profile = "black" 8 | multi_line_output = 3 9 | 10 | [tool.ruff] 11 | line-length = 100 12 | builtins = ["_"] 13 | output-format = "grouped" 14 | 15 | [tool.ruff.lint] 16 | extend-select = ["B", "SIM", "PIE", "C4", "INT", "LOG"] 17 | extend-ignore = ["E402", "E731", "B905"] 18 | -------------------------------------------------------------------------------- /cozy/ui/list_box_separator_row.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | 4 | class ListBoxSeparatorRow(Gtk.ListBoxRow): 5 | """ 6 | This class represents a separator in a listbox row. 7 | """ 8 | 9 | def __init__(self): 10 | super().__init__() 11 | separator = Gtk.Separator() 12 | self.set_child(separator) 13 | self.set_sensitive(False) 14 | self.props.selectable = False -------------------------------------------------------------------------------- /cozy/db/collation.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def collate_natural(s1, s2): 5 | if s1 == s2: 6 | return 0 7 | 8 | convert = lambda text: int(text) if text.isdigit() else text.lower() 9 | alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] 10 | list = sorted([s1, s2], key=alphanum_key) 11 | 12 | if list.index(s1) == 0: 13 | return -1 14 | else: 15 | return 1 16 | -------------------------------------------------------------------------------- /cozy/db/settings.py: -------------------------------------------------------------------------------- 1 | from peewee import BooleanField, CharField, ForeignKeyField, IntegerField 2 | 3 | from cozy.db.book import Book 4 | from cozy.db.model_base import ModelBase 5 | 6 | DB_VERSION = 10 7 | 8 | 9 | class Settings(ModelBase): 10 | path = CharField() 11 | first_start = BooleanField(default=True) 12 | last_played_book = ForeignKeyField(Book, null=True) 13 | version = IntegerField(default=DB_VERSION) 14 | -------------------------------------------------------------------------------- /cozy/architecture/profiler.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import time 4 | 5 | log = logging.getLogger("timing") 6 | 7 | def timing(f): 8 | @functools.wraps(f) 9 | def wrap(*args): 10 | time1 = time.perf_counter() 11 | ret = f(*args) 12 | time2 = time.perf_counter() 13 | log.info('%s function took %.3f ms', f.__name__, (time2-time1)*1000.0) 14 | 15 | return ret 16 | return wrap -------------------------------------------------------------------------------- /cozy/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class OpenView(Enum): 5 | AUTHOR = auto() 6 | READER = auto() 7 | BOOK = auto() 8 | LIBRARY = auto() 9 | BACK = auto() 10 | 11 | 12 | class View(Enum): 13 | EMPTY_STATE = auto() 14 | PREPARING_LIBRARY = auto() 15 | LIBRARY = auto() 16 | LIBRARY_FILTER = auto() 17 | LIBRARY_BOOKS = auto() 18 | BOOK_DETAIL = auto() 19 | NO_MEDIA = auto() 20 | -------------------------------------------------------------------------------- /cozy/ui/toaster.py: -------------------------------------------------------------------------------- 1 | import inject 2 | from gi.repository import Adw, Gtk 3 | 4 | 5 | class ToastNotifier: 6 | _builder: Gtk.Builder = inject.attr("MainWindowBuilder") 7 | 8 | def __init__(self) -> None: 9 | super().__init__() 10 | 11 | self.overlay: Adw.ToastOverlay = self._builder.get_object("toast_overlay") 12 | 13 | def show(self, message: str) -> None: 14 | self.overlay.add_toast(Adw.Toast(title=message, timeout=2)) 15 | 16 | -------------------------------------------------------------------------------- /data/com.github.geigi.cozy.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Cozy 3 | GenericName=Audio Book Player 4 | Comment=Play and organize your audio book collection 5 | Icon=com.github.geigi.cozy 6 | Exec=com.github.geigi.cozy %U 7 | Terminal=false 8 | Type=Application 9 | Categories=GNOME;GTK;AudioVideo;Player;Audio; 10 | StartupNotify=true 11 | MimeType=x-content/audio-player; 12 | 13 | # Translators: Do NOT translate or transliterate this text (these are enum types)! 14 | X-Purism-FormFactor=Workstation;Mobile; 15 | -------------------------------------------------------------------------------- /data/ui/progress_popover.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ProgressPopover: Popover { 5 | Adw.Clamp { 6 | margin-start: 15; 7 | margin-end: 15; 8 | margin-top: 15; 9 | margin-bottom: 15; 10 | maximum-size: 300; 11 | tightening-threshold: 250; 12 | 13 | Box { 14 | orientation: vertical; 15 | spacing: 10; 16 | 17 | Label progress_label { 18 | ellipsize: end; 19 | xalign: 0; 20 | } 21 | 22 | ProgressBar progress_bar { 23 | fraction: 0.5; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cozy/db/book.py: -------------------------------------------------------------------------------- 1 | from peewee import BlobField, BooleanField, CharField, FloatField, IntegerField 2 | 3 | from cozy.db.model_base import ModelBase 4 | 5 | 6 | class Book(ModelBase): 7 | name = CharField() 8 | author = CharField() 9 | reader = CharField() 10 | position = IntegerField() 11 | rating = IntegerField() 12 | cover = BlobField(null=True) 13 | playback_speed = FloatField(default=1.0) 14 | last_played = IntegerField(default=0) 15 | offline = BooleanField(default=False) 16 | downloaded = BooleanField(default=False) 17 | hidden = BooleanField(default=False) 18 | -------------------------------------------------------------------------------- /cozy/ui/widgets/progress_popover.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk 2 | 3 | 4 | @Gtk.Template.from_resource('/com/github/geigi/cozy/ui/progress_popover.ui') 5 | class ProgressPopover(Gtk.Popover): 6 | __gtype_name__ = 'ProgressPopover' 7 | 8 | progress_label: Gtk.Label = Gtk.Template.Child() 9 | progress_bar: Gtk.ProgressBar = Gtk.Template.Child() 10 | 11 | def __init__(self): 12 | super().__init__() 13 | 14 | def set_message(self, message: str): 15 | self.progress_label.set_text(message) 16 | 17 | def set_progress(self, progress: float): 18 | self.progress_bar.set_fraction(progress) 19 | 20 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.folder-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/ui/storage_row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $StorageRow: Adw.ActionRow { 5 | selectable: false; 6 | activatable: true; 7 | 8 | [prefix] 9 | Image icon {} 10 | 11 | [suffix] 12 | Image default_icon { 13 | visible: false; 14 | margin-start: 6; 15 | margin-end: 6; 16 | icon-name: 'cozy.default-storage-symbolic'; 17 | 18 | styles [ 19 | "accent", 20 | ] 21 | } 22 | 23 | [suffix] 24 | MenuButton menu_button { 25 | icon-name: 'view-more-symbolic'; 26 | valign: center; 27 | halign: center; 28 | 29 | styles [ 30 | "flat", 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | _build/ 4 | .buildconfig 5 | .flatpak-builder 6 | build/ 7 | build-fp/ 8 | app/ 9 | bin/ 10 | cozy/__pycache__ 11 | debian/build/ 12 | debian/com.github.geigi.cozy.debhelper.log 13 | debian/com.github.geigi.cozy.substvars 14 | debian/com.github.geigi.cozy/ 15 | debian/debhelper-build-stamp 16 | debian/files 17 | peewee/ 18 | venv/ 19 | env/ 20 | blueprint-compiler/ 21 | 22 | .pytest_cache 23 | 24 | 25 | # Ignore glade tmp files 26 | *~ 27 | \#*\# 28 | 29 | # Ignore sublime files 30 | *.sublime-workspace 31 | *.sublime-project 32 | 33 | .vscode/.ropeproject 34 | .vscode/tags 35 | .tx 36 | *.log 37 | *.prof 38 | *.pyc 39 | 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /.ci/obs_wait_for_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo $(osc r) 4 | osc r | grep -E "building|blocked|scheduled" 5 | builds_in_progress=$? 6 | counter=0 7 | 8 | while [[ $builds_in_progress != [1] ]] 9 | do 10 | echo "Build(s) in progress." 11 | sleep 5 12 | echo $(osc r) 13 | osc r | grep -E "building|scheduled" 14 | builds_in_progress=$? 15 | counter=$((counter+5)) 16 | if (( counter > 3600 )); then 17 | echo "Build longer than 60min, failing!" 18 | exit 1 19 | fi 20 | done 21 | 22 | echo $(osc r) 23 | osc r | grep -E "broken|failed" 24 | build_successful=$? 25 | if (( $build_successful != 1 )); then 26 | echo "At least one build failed." 27 | exit 1 28 | fi 29 | 30 | echo "Builds succeeded." 31 | exit 0 32 | -------------------------------------------------------------------------------- /cozy/media/media_file.py: -------------------------------------------------------------------------------- 1 | 2 | from cozy.media.chapter import Chapter 3 | 4 | 5 | class MediaFile: 6 | book_name: str 7 | author: str 8 | reader: str 9 | disk: int 10 | cover: bytes 11 | path: str 12 | modified: int 13 | chapters: list[Chapter] 14 | 15 | def __init__(self, book_name: str, author: str, reader: str, disk: int, cover: bytes, path: str, modified: int, 16 | chapters: list[Chapter]): 17 | self.book_name = book_name 18 | self.author = author 19 | self.reader = reader 20 | self.disk = disk 21 | self.cover = cover 22 | self.path = path 23 | self.modified = modified 24 | self.chapters = chapters 25 | -------------------------------------------------------------------------------- /test/cozy/media/test_files.py: -------------------------------------------------------------------------------- 1 | def test_update_copy_status_emits_1_for_zero_files(mocker): 2 | from cozy.media.files import Files 3 | 4 | files = Files() 5 | spy = mocker.spy(files, "emit_event_main_thread") 6 | 7 | files._file_count = 0 8 | files._update_copy_status(1, 2, None) 9 | spy.assert_called_once_with("copy-progress", 1.0) 10 | 11 | 12 | def test_update_copy_status_emits_1_for_zero_byte_files(mocker): 13 | from cozy.media.files import Files 14 | 15 | files = Files() 16 | spy = mocker.spy(files, "emit_event_main_thread") 17 | 18 | files._file_count = 1 19 | files._file_progess = 1 20 | files._update_copy_status(0, 0, None) 21 | 22 | spy.assert_called_once_with("copy-progress", 1.0) 23 | -------------------------------------------------------------------------------- /data/ui/meson.build: -------------------------------------------------------------------------------- 1 | message('Compiling blueprints') 2 | 3 | 4 | blueprints = custom_target('blueprints', 5 | input: files( 6 | 'book_card.blp', 7 | 'book_detail.blp', 8 | 'chapter_element.blp', 9 | 'error_reporting.blp', 10 | 'headerbar.blp', 11 | 'main_window.blp', 12 | 'media_controller.blp', 13 | 'playback_speed_popover.blp', 14 | 'preferences.blp', 15 | 'progress_popover.blp', 16 | 'search_page.blp', 17 | 'seek_bar.blp', 18 | 'storage_locations.blp', 19 | 'storage_row.blp', 20 | 'welcome_dialog.blp', 21 | 'sleep_timer_dialog.blp', 22 | ), 23 | output: '.', 24 | command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], 25 | ) 26 | -------------------------------------------------------------------------------- /cozy/view_model/app_view_model.py: -------------------------------------------------------------------------------- 1 | from cozy.architecture.event_sender import EventSender 2 | from cozy.architecture.observable import Observable 3 | from cozy.enums import View 4 | 5 | 6 | class AppViewModel(Observable, EventSender): 7 | def __init__(self): 8 | super().__init__() 9 | super(Observable, self).__init__() 10 | 11 | self._view = View.EMPTY_STATE 12 | 13 | def open_book_detail_view(self): 14 | self._notify("open_book_overview") 15 | 16 | @property 17 | def view(self) -> View: 18 | return self._view 19 | 20 | @view.setter 21 | def view(self, new_value: View): 22 | self._view = new_value 23 | self._notify("view") 24 | self.emit_event_main_thread("view", self._view) 25 | 26 | -------------------------------------------------------------------------------- /data/ui/playback_speed_popover.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | Adjustment speed_value { 4 | lower: 0.5; 5 | upper: 3.5; 6 | value: 1; 7 | step-increment: 0.05; 8 | page-increment: 0.1; 9 | } 10 | 11 | template $PlaybackSpeedPopover: Popover { 12 | width-request: 300; 13 | 14 | child: Box { 15 | Scale playback_speed_scale { 16 | hexpand: true; 17 | focusable: true; 18 | margin-start: 6; 19 | margin-end: 6; 20 | margin-top: 6; 21 | margin-bottom: 6; 22 | adjustment: speed_value; 23 | fill-level: 5; 24 | round-digits: 1; 25 | } 26 | 27 | Label playback_speed_label { 28 | margin-end: 12; 29 | label: '1.0 x'; 30 | use-markup: true; 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /cozy/control/application_directories.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from gi.repository import GLib 4 | 5 | 6 | def get_path_relative_to_chache_folder(*args) -> Path: 7 | dir = Path(GLib.get_user_cache_dir(), "cozy", *args) 8 | dir.mkdir(parents=True, exist_ok=True) 9 | return dir 10 | 11 | 12 | def get_artwork_cache_dir() -> Path: 13 | return get_path_relative_to_chache_folder("artwork") 14 | 15 | 16 | def get_offline_cache_dir() -> Path: 17 | return get_path_relative_to_chache_folder("offline") 18 | 19 | 20 | def get_cache_dir() -> Path: 21 | return get_path_relative_to_chache_folder() 22 | 23 | 24 | def get_data_dir() -> Path: 25 | dir = Path(GLib.get_user_data_dir(), "cozy") 26 | dir.mkdir(parents=True, exist_ok=True) 27 | 28 | return dir 29 | -------------------------------------------------------------------------------- /cozy/ui/widgets/search_results.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from gi.repository import Adw, Gtk 4 | 5 | 6 | class ArtistResultRow(Adw.ActionRow): 7 | def __init__(self, name: str, on_click: Callable[[str], None]) -> None: 8 | super().__init__( 9 | title=name, 10 | selectable=False, 11 | activatable=True, 12 | use_markup=False, 13 | tooltip_text=_("Jump to {artist_name}").format(artist_name=name), 14 | ) 15 | 16 | self.connect("activated", lambda *_: on_click(name)) 17 | 18 | icon = Gtk.Image.new_from_icon_name("cozy.author-symbolic") 19 | icon.set_pixel_size(24) 20 | icon.set_margin_top(6) 21 | icon.set_margin_bottom(6) 22 | 23 | self.add_prefix(icon) 24 | -------------------------------------------------------------------------------- /test/cozy/model/storage_block_list.py: -------------------------------------------------------------------------------- 1 | import inject 2 | import pytest 3 | from peewee import SqliteDatabase 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def setup_inject(peewee_database_storage): 8 | inject.clear_and_configure(lambda binder: binder.bind(SqliteDatabase, peewee_database_storage)) 9 | yield 10 | inject.clear() 11 | 12 | 13 | def test_rebase_path(): 14 | from cozy.db.storage_blacklist import StorageBlackList 15 | from cozy.model.storage_block_list import StorageBlockList 16 | 17 | model = StorageBlockList() 18 | 19 | model.rebase_path("/path/to/replace", "/replaced/path") 20 | 21 | assert StorageBlackList.get_by_id(1).path == "/replaced/path/test1.mp3" 22 | assert StorageBlackList.get_by_id(2).path == "/path/to/not/replace/test2.mp3" 23 | -------------------------------------------------------------------------------- /cozy/db/model_base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from peewee import Model 4 | from playhouse.sqliteq import SqliteQueueDatabase 5 | 6 | from cozy.control.application_directories import get_data_dir 7 | 8 | log = logging.getLogger("db") 9 | 10 | _db = None 11 | 12 | 13 | def get_sqlite_database(): 14 | global _db 15 | return _db 16 | 17 | 18 | def database_file_exists(): 19 | return (get_data_dir() / "cozy.db").is_file() 20 | 21 | 22 | def __open_database(): 23 | global _db 24 | 25 | _db = SqliteQueueDatabase(str(get_data_dir() / "cozy.db"), queue_max_size=128, results_timeout=15.0, 26 | timeout=15.0, pragmas=[('cache_size', -1024 * 32), ('journal_mode', 'wal')]) 27 | 28 | 29 | __open_database() 30 | 31 | 32 | class ModelBase(Model): 33 | class Meta: 34 | database = _db 35 | -------------------------------------------------------------------------------- /data/ui/chapter_element.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ChapterElement: Adw.ActionRow { 5 | selectable: true; 6 | activatable: true; 7 | use-markup: false; 8 | tooltip-text: _("Play this part"); 9 | 10 | [prefix] 11 | Stack icon_stack { 12 | StackPage { 13 | name: 'number'; 14 | 15 | child: Label number_label { 16 | width-chars: 3; 17 | 18 | styles [ 19 | "dim-label", 20 | ] 21 | }; 22 | } 23 | 24 | StackPage { 25 | name: 'icon'; 26 | 27 | child: Image play_icon { 28 | icon-name: 'media-playback-start-symbolic'; 29 | halign: center; 30 | valign: center; 31 | }; 32 | } 33 | } 34 | 35 | [suffix] 36 | Label duration_label { 37 | styles [ 38 | "dim-label", 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cozy/architecture/event_sender.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from gi.repository import GLib 4 | 5 | 6 | class EventSender: 7 | _listeners: list[Callable] 8 | 9 | def __init__(self): 10 | self._listeners = [] 11 | 12 | def emit_event(self, event, message=None): 13 | if isinstance(event, tuple) and message is None: 14 | event, message = event 15 | 16 | for function in self._listeners: 17 | function(event, message) 18 | 19 | def emit_event_main_thread(self, event: str, message=None): 20 | GLib.MainContext.default().invoke_full(GLib.PRIORITY_DEFAULT_IDLE, self.emit_event, (event, message)) 21 | 22 | def add_listener(self, function: Callable[[str, object], None]): 23 | self._listeners.append(function) 24 | 25 | def destroy_listeners(self): 26 | self._listeners = [] 27 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.default-storage-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/ui/storage_locations.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $StorageLocations: Adw.PreferencesGroup { 5 | title: _("Storage Locations"); 6 | 7 | ListBox storage_locations_list { 8 | margin-bottom: 18; 9 | 10 | styles [ 11 | "boxed-list", 12 | ] 13 | } 14 | } 15 | 16 | Adw.ButtonRow new_storage_row { 17 | title: _("Add Storage"); 18 | start-icon-name: "list-add-symbolic"; 19 | activated => $_on_new_storage_clicked(); 20 | } 21 | 22 | menu storage_menu { 23 | section { 24 | item { 25 | label: _("External drive"); 26 | action: 'storage.mark-external'; 27 | } 28 | } 29 | 30 | section { 31 | item { 32 | label: _("Set as default"); 33 | action: 'storage.make-default'; 34 | } 35 | 36 | item { 37 | label: _("Remove"); 38 | action: 'storage.remove'; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cozy/ui/list_box_row_with_data.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, Pango 2 | 3 | 4 | class ListBoxRowWithData(Gtk.ListBoxRow): 5 | """ 6 | This class represents a listboxitem for an author/reader. 7 | """ 8 | LABEL_MARGIN = 8 9 | 10 | def __init__(self, data, bold=False, **properties): 11 | super().__init__(**properties) 12 | self.data = data 13 | 14 | self.set_margin_bottom(3) 15 | 16 | self.add_css_class("filter-list-box-row") 17 | 18 | label = Gtk.Label.new(data) 19 | if bold: 20 | label.set_markup("" + data + "") 21 | label.set_xalign(0.0) 22 | label.set_margin_top(self.LABEL_MARGIN) 23 | label.set_margin_bottom(self.LABEL_MARGIN) 24 | label.set_margin_start(6) 25 | label.set_max_width_chars(30) 26 | label.set_ellipsize(Pango.EllipsizeMode.END) 27 | self.set_child(label) 28 | self.set_tooltip_text(data) 29 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.volume-low-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /flatpak/com.github.geigi.cozy.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "com.github.geigi.cozy", 3 | "runtime": "org.gnome.Platform", 4 | "runtime-version": "master", 5 | "sdk": "org.gnome.Sdk", 6 | "command": "com.github.geigi.cozy", 7 | "finish-args": [ 8 | "--share=ipc", 9 | "--share=network", 10 | "--socket=fallback-x11", 11 | "--socket=wayland", 12 | "--socket=pulseaudio", 13 | "--device=dri", 14 | "--filesystem=host", 15 | "--filesystem=xdg-run/gvfs", 16 | "--filesystem=xdg-run/gvfsd", 17 | "--talk-name=org.gtk.vfs.*", 18 | "--talk-name=org.gnome.SessionManager", 19 | "--system-talk-name=org.freedesktop.login1" 20 | ], 21 | "modules": [ 22 | "python-deps.json", 23 | { 24 | "name": "cozy", 25 | "buildsystem": "meson", 26 | "run-tests": true, 27 | "sources": [ 28 | { 29 | "type": "dir", 30 | "path": "../." 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.rewind-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.forward-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /cozy/report/reporter.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from multiprocessing.pool import ThreadPool as Pool 3 | 4 | from cozy.report.log_level import LogLevel 5 | from cozy.report.report_to_loki import report 6 | 7 | report_pool = Pool(5) 8 | 9 | def info(component: str, message: str): 10 | report_pool.apply_async(report, [component, LogLevel.INFO, message, None]) 11 | 12 | 13 | def warning(component: str, message: str): 14 | report_pool.apply_async(report, [component, LogLevel.WARNING, message, None]) 15 | 16 | 17 | def error(component: str, message: str): 18 | report_pool.apply_async(report, [component, LogLevel.ERROR, message, None]) 19 | 20 | 21 | def exception(component: str, exception: Exception, message=None): 22 | if not message: 23 | message = traceback.format_exc() 24 | 25 | report_pool.apply_async(report, [component, LogLevel.ERROR, message, exception]) 26 | 27 | 28 | def close(): 29 | report_pool.close() 30 | report_pool.terminate() 31 | -------------------------------------------------------------------------------- /cozy/ui/widgets/filter_list_box.py: -------------------------------------------------------------------------------- 1 | 2 | from gi.repository import Gtk 3 | 4 | from cozy.ui.list_box_row_with_data import ListBoxRowWithData 5 | 6 | 7 | class FilterListBox(Gtk.ListBox): 8 | __gtype_name__ = 'FilterListBox' 9 | 10 | def __init__(self, **properties): 11 | super().__init__(**properties) 12 | 13 | def populate(self, elements: list[str]): 14 | self.remove_all() 15 | 16 | all_row = ListBoxRowWithData(_("All"), True) 17 | all_row.set_tooltip_text(_("Display all books")) 18 | self.append(all_row) 19 | self.select_row(all_row) 20 | 21 | for element in elements: 22 | row = ListBoxRowWithData(element, False) 23 | self.append(row) 24 | 25 | def select_row_with_content(self, row_content: str): 26 | for child in self: 27 | if isinstance(child, ListBoxRowWithData) and child.data == row_content: 28 | self.select_row(child) 29 | break 30 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.author-symbolic.svg: -------------------------------------------------------------------------------- 1 | 5 | 13 | 21 | -------------------------------------------------------------------------------- /cozy/view_model/settings_view_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import inject 4 | 5 | from cozy.architecture.event_sender import EventSender 6 | from cozy.architecture.observable import Observable 7 | from cozy.media.importer import Importer 8 | from cozy.model.settings import Settings 9 | from cozy.settings import ApplicationSettings 10 | 11 | log = logging.getLogger("settings_view_model") 12 | 13 | 14 | class SettingsViewModel(Observable, EventSender): 15 | _importer: Importer = inject.attr(Importer) 16 | _model: Settings = inject.attr(Settings) 17 | _app_settings: ApplicationSettings = inject.attr(ApplicationSettings) 18 | 19 | def __init__(self): 20 | super().__init__() 21 | super(Observable, self).__init__() 22 | 23 | self._lock_ui: bool = False 24 | 25 | @property 26 | def lock_ui(self) -> bool: 27 | return self._lock_ui 28 | 29 | @lock_ui.setter 30 | def lock_ui(self, new_value: bool): 31 | self._lock_ui = new_value 32 | self._notify("lock_ui") 33 | -------------------------------------------------------------------------------- /cozy/tools.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | 5 | # https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread-in-python 6 | class StoppableThread(threading.Thread): 7 | """Thread class with a stop() method. The thread itself has to check 8 | regularly for the stopped() condition.""" 9 | 10 | def __init__(self, target=None): 11 | super().__init__(target=target) 12 | self._stop_event = threading.Event() 13 | 14 | def stop(self): 15 | self._stop_event.set() 16 | 17 | def stopped(self): 18 | return self._stop_event.is_set() 19 | 20 | 21 | # From https://stackoverflow.com/questions/11488877/periodically-execute-function-in-thread-in-real-time-every-n-seconds 22 | class IntervalTimer(StoppableThread): 23 | 24 | def __init__(self, interval, worker_func): 25 | super().__init__() 26 | self._interval = interval 27 | self._worker_func = worker_func 28 | 29 | def run(self): 30 | while not self.stopped(): 31 | self._worker_func() 32 | time.sleep(self._interval) 33 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.volume-muted-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /cozy/ui/delete_book_view.py: -------------------------------------------------------------------------------- 1 | import inject 2 | from gi.repository import Adw, Gtk 3 | 4 | from cozy.model.book import Book 5 | from cozy.ui.widgets.book_row import BookRow 6 | 7 | 8 | class DeleteBookView(Adw.AlertDialog): 9 | main_window = inject.attr("MainWindow") 10 | 11 | def __init__(self, callback, book: Book): 12 | super().__init__( 13 | heading=_("Remove Audiobook From Library?"), 14 | body=_("This audiobook will be removed from Cozy's library. To be able to listen to it again, you will need to remove, and re-add its storage location."), 15 | default_response="cancel", 16 | close_response="cancel", 17 | ) 18 | 19 | self.add_response("cancel", _("Cancel")) 20 | self.add_response("remove", _("Remove")) 21 | self.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE) 22 | 23 | list_box = Gtk.ListBox(margin_top=12, css_classes=["boxed-list"]) 24 | list_box.append(BookRow(book)) 25 | self.set_extra_child(list_box) 26 | 27 | self.connect("response", callback, book) 28 | 29 | def present(self) -> None: 30 | super().present(self.main_window.window) 31 | -------------------------------------------------------------------------------- /test/cozy/model/test_split_strings_to_set.py: -------------------------------------------------------------------------------- 1 | from cozy.model.library import split_strings_to_set 2 | 3 | 4 | def test_split_strings_does_nothing_for_non_seperated_element(): 5 | test = "This is a nice test. Nothing should be split." 6 | result = split_strings_to_set({test}) 7 | 8 | assert {test} == result 9 | 10 | 11 | def test_split_strings_are_splitted(): 12 | test = "This/is&a,test;splitting" 13 | how_it_should_be = {"This", "is", "a", "test", "splitting"} 14 | 15 | result = split_strings_to_set({test}) 16 | 17 | assert how_it_should_be == result 18 | 19 | 20 | def test_split_strings_are_splitted_and_whitespace_is_removed(): 21 | test = "This / is & a , test ; splitting " 22 | how_it_should_be = {"This", "is", "a", "test", "splitting"} 23 | 24 | result = split_strings_to_set({test}) 25 | 26 | assert how_it_should_be == result 27 | 28 | 29 | def test_split_strings_are_splitted_and_dots_are_not_splitted(): 30 | test = "This Dr. Prof. L. Lala / is & a , test ; splitting " 31 | how_it_should_be = {"This Dr. Prof. L. Lala", "is", "a", "test", "splitting"} 32 | 33 | result = split_strings_to_set({test}) 34 | 35 | assert how_it_should_be == result 36 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.recent-symbolic.svg: -------------------------------------------------------------------------------- 1 | 5 | 13 | 23 | -------------------------------------------------------------------------------- /cozy/ui/db_migration_failed_view.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | 3 | from gi.repository import Adw 4 | 5 | EXPLANATION = _("During an update of the database an error occurred and Cozy will not be able to startup.\ 6 | A backup of the database was created before the update and has been restored now.\ 7 | Until this issue is resolved please use version 0.9.5 of Cozy.\ 8 | You can help resolve this problem by reporting an issue on GitHub.") 9 | 10 | 11 | class DBMigrationFailedView(Adw.AlertDialog): 12 | def __init__(self): 13 | super().__init__( 14 | heading=_("Failed to Update Database"), 15 | body=EXPLANATION, 16 | default_response="help", 17 | close_response="close", 18 | ) 19 | 20 | self.add_response("close", _("Close Cozy")) 21 | self.add_response("help", _("Receive help on GitHub")) 22 | self.set_response_appearance("help", Adw.ResponseAppearance.SUGGESTED) 23 | 24 | self.connect("response", self.get_help) 25 | 26 | def get_help(self, _, response): 27 | if response == "help": 28 | webbrowser.open("https://github.com/geigi/cozy/issues", new=2) 29 | 30 | def present(self) -> None: 31 | super().present() 32 | 33 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | cozy/application.py 2 | cozy/control/mpris.py 3 | cozy/control/offline_cache.py 4 | cozy/media/files.py 5 | cozy/media/importer.py 6 | cozy/media/tag_reader.py 7 | cozy/model/track.py 8 | cozy/tools.py 9 | cozy/ui/about_window.py 10 | cozy/ui/book_detail_view.py 11 | cozy/ui/chapter_element.py 12 | cozy/ui/db_migration_failed_view.py 13 | cozy/ui/delete_book_view.py 14 | cozy/ui/file_not_found_dialog.py 15 | cozy/ui/import_failed_dialog.py 16 | cozy/ui/main_view.py 17 | cozy/ui/preferences_window.py 18 | cozy/ui/widgets/book_card.py 19 | cozy/ui/widgets/book_row.py 20 | cozy/ui/widgets/error_reporting.py 21 | cozy/ui/widgets/filter_list_box.py 22 | cozy/ui/widgets/search_results.py 23 | cozy/ui/widgets/sleep_timer.py 24 | cozy/ui/widgets/storages.py 25 | cozy/view_model/headerbar_view_model.py 26 | cozy/view_model/library_view_model.py 27 | data/ui/album_element.blp 28 | data/ui/book_card.blp 29 | data/ui/book_detail.blp 30 | data/ui/chapter_element.blp 31 | data/ui/error_reporting.blp 32 | data/ui/first_import_button.blp 33 | data/ui/headerbar.blp 34 | data/ui/main_window.blp 35 | data/ui/media_controller.blp 36 | data/ui/playback_speed_popover.blp 37 | data/ui/preferences.blp 38 | data/ui/progress_popover.blp 39 | data/ui/search_page.blp 40 | data/ui/seek_bar.blp 41 | data/ui/storage_locations.blp 42 | data/ui/storage_row.blp 43 | data/ui/timer_popover.blp 44 | main.py 45 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.timer-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/ui/error_reporting.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | 5 | template $ErrorReporting: Box { 6 | orientation: vertical; 7 | spacing: 8; 8 | 9 | Label { 10 | halign: fill; 11 | hexpand: true; 12 | label: C_("Error and crash reporting dialog", "You can help us improve Cozy by contributing information in case of errors and crashes."); 13 | wrap: true; 14 | xalign: 0; 15 | } 16 | 17 | Label { 18 | halign: fill; 19 | hexpand: true; 20 | label: C_("Error and crash reporting dialog", "Contributing this information is optional and completely anonymous. We will never collect personal data, files you import or any information that could identify you."); 21 | wrap: true; 22 | xalign: 0; 23 | } 24 | 25 | Label { 26 | halign: fill; 27 | hexpand: true; 28 | label: C_("Error and crash reporting dialog", "Cozy is developed in the open, and the error reporting code can be inspected here:"); 29 | wrap: true; 30 | xalign: 0; 31 | } 32 | 33 | LinkButton { 34 | label: _("Sourcecode on GitHub"); 35 | uri: "https://github.com/geigi/cozy/tree/master/cozy/report"; 36 | margin-bottom: 6; 37 | } 38 | 39 | ListBox { 40 | styles ["boxed-list"] 41 | selection-mode: none; 42 | 43 | Adw.ComboRow detail_combo { 44 | title: _("Detail Level"); 45 | } 46 | 47 | Adw.ActionRow description {} 48 | } 49 | } -------------------------------------------------------------------------------- /data/ui/seek_bar.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | Adjustment seek_bar_adjustment { 4 | upper: 100; 5 | step-increment: 1; 6 | page-increment: 15; 7 | } 8 | 9 | template $SeekBar: Box { 10 | valign: center; 11 | hexpand: true; 12 | spacing: 5; 13 | 14 | Label current_label { 15 | tooltip-text: _("Elapsed time"); 16 | halign: end; 17 | valign: center; 18 | label: '--:--'; 19 | single-line-mode: true; 20 | 21 | accessibility { 22 | label: _("Elapsed time of current part"); 23 | } 24 | 25 | styles [ 26 | "numeric", 27 | ] 28 | } 29 | 30 | Scale progress_scale { 31 | width-request: 150; 32 | focusable: true; 33 | tooltip-text: _("Jump to position in current chapter"); 34 | valign: center; 35 | hexpand: true; 36 | adjustment: seek_bar_adjustment; 37 | restrict-to-fill-level: false; 38 | fill-level: 0; 39 | 40 | accessibility { 41 | label: _("Position of the current part in seconds"); 42 | } 43 | } 44 | 45 | Box remaining_event_box { 46 | valign: center; 47 | 48 | Label remaining_label { 49 | tooltip-text: _("Remaining time"); 50 | halign: start; 51 | label: '--:--'; 52 | single-line-mode: true; 53 | 54 | accessibility { 55 | label: _("Remaining time of current part"); 56 | } 57 | 58 | styles [ 59 | "numeric", 60 | ] 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.book-open-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 16 | 18 | image/svg+xml 19 | 21 | 22 | 23 | 24 | 25 | 27 | 30 | 31 | -------------------------------------------------------------------------------- /cozy/media/media_detector.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | 4 | from gi.repository import Gst, GstPbutils 5 | 6 | from cozy.architecture.event_sender import EventSender 7 | from cozy.media.media_file import MediaFile 8 | from cozy.media.tag_reader import TagReader 9 | 10 | log = logging.getLogger("media_detector") 11 | 12 | 13 | class NotAnAudioFile(Exception): 14 | pass 15 | 16 | 17 | class AudioFileCouldNotBeDiscovered(Exception): 18 | pass 19 | 20 | 21 | class MediaDetector(EventSender): 22 | def __init__(self, path: str): 23 | super().__init__() 24 | self.uri = pathlib.Path(path).absolute().as_uri() 25 | 26 | Gst.init(None) 27 | self.discoverer: GstPbutils.Discoverer = GstPbutils.Discoverer() 28 | 29 | def get_media_data(self) -> MediaFile: 30 | try: 31 | discoverer_info = self.discoverer.discover_uri(self.uri) 32 | except Exception: 33 | log.info("Skipping file because it couldn't be detected: %s", self.uri) 34 | raise AudioFileCouldNotBeDiscovered(self.uri) from None 35 | 36 | if self._is_valid_audio_file(discoverer_info): 37 | return TagReader(self.uri, discoverer_info).get_tags() 38 | else: 39 | raise AudioFileCouldNotBeDiscovered(self.uri) 40 | 41 | def _is_valid_audio_file(self, info: GstPbutils.DiscovererInfo): 42 | return len(info.get_audio_streams()) == 1 and not info.get_video_streams() 43 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.volume-high-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /cozy/ui/widgets/playback_speed_popover.py: -------------------------------------------------------------------------------- 1 | import inject 2 | from gi.repository import Gtk 3 | 4 | from cozy.view_model.playback_speed_view_model import PlaybackSpeedViewModel 5 | 6 | 7 | @Gtk.Template.from_resource('/com/github/geigi/cozy/ui/playback_speed_popover.ui') 8 | class PlaybackSpeedPopover(Gtk.Popover): 9 | __gtype_name__ = "PlaybackSpeedPopover" 10 | 11 | _view_model: PlaybackSpeedViewModel = inject.attr(PlaybackSpeedViewModel) 12 | 13 | playback_speed_scale: Gtk.Scale = Gtk.Template.Child() 14 | playback_speed_label: Gtk.Label = Gtk.Template.Child() 15 | 16 | def __init__(self, **kwargs): 17 | super().__init__(**kwargs) 18 | 19 | self.playback_speed_scale.add_mark(1.0, Gtk.PositionType.RIGHT, None) 20 | self.playback_speed_scale.set_increments(0.02, 0.05) 21 | self.playback_speed_scale.connect("value-changed", self._on_playback_speed_scale_changed) 22 | 23 | self._view_model.bind_to("playback_speed", self._on_playback_speed_changed) 24 | 25 | self._on_playback_speed_changed() 26 | 27 | def _on_playback_speed_scale_changed(self, _): 28 | speed = round(self.playback_speed_scale.get_value(), 2) 29 | self._view_model.playback_speed = speed 30 | 31 | self.playback_speed_label.set_markup(f"{speed:3.1f} x") 32 | 33 | def _on_playback_speed_changed(self): 34 | self.playback_speed_scale.set_value(self._view_model.playback_speed) 35 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.volume-medium-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.ci/flathub_wait_for_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | URL_RUNNING_BUILDS="https://flathub.org/builds/api/v2/builders/32/builds?complete=false&flathub_name__eq=com.github.geigi.cozy&order=-number&property=owners&property=workername" 4 | URL_LAST_BUILD="https://flathub.org/builds/api/v2/builders/32/builds?flathub_name__eq=com.github.geigi.cozy&flathub_repo_status__gt=1&limit=1&order=-number&property=owners&property=workername" 5 | 6 | function wait_for_build_triggered { 7 | for i in {0..30} 8 | do 9 | sleep 1 10 | builds_in_progress=$(curl $URL_RUNNING_BUILDS | json meta.total) 11 | if (( builds_in_progress > 0 )); then 12 | echo "$builds_in_progress build(s) in progress." 13 | return 0 14 | fi 15 | done 16 | 17 | echo "No build in progress." 18 | return 1 19 | } 20 | 21 | wait_for_build_triggered 22 | build_triggered=$? 23 | if (( $build_triggered > 0 )); then 24 | exit 1 25 | fi 26 | 27 | builds_in_progress=$(curl $URL_RUNNING_BUILDS | json meta.total) 28 | counter=0 29 | 30 | while [[ $builds_in_progress != [0] ]] 31 | do 32 | echo "$builds_in_progress build(s) in progress." 33 | sleep 5 34 | builds_in_progress=$(curl $URL_RUNNING_BUILDS | json meta.total) 35 | counter=$((counter+5)) 36 | if (( counter > 1800 )); then 37 | echo "Build longer than 30min, failing!" 38 | exit 1 39 | fi 40 | done 41 | 42 | result=$(curl $URL_LAST_BUILD | json builds[0].results) 43 | if (( builds_in_progress > 0 )); then 44 | echo "Build failed." 45 | exit 1 46 | fi 47 | 48 | echo "Build succeeded." 49 | exit 0 50 | -------------------------------------------------------------------------------- /data/gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | style.css 5 | ui/book_card.ui 6 | ui/book_detail.ui 7 | ui/chapter_element.ui 8 | ui/error_reporting.ui 9 | ui/headerbar.ui 10 | ui/main_window.ui 11 | ui/media_controller.ui 12 | ui/playback_speed_popover.ui 13 | ui/preferences.ui 14 | ui/progress_popover.ui 15 | ui/search_page.ui 16 | ui/seek_bar.ui 17 | ui/storage_locations.ui 18 | ui/storage_row.ui 19 | ui/welcome_dialog.ui 20 | ui/sleep_timer_dialog.ui 21 | 22 | 23 | ../AUTHORS.md 24 | com.github.geigi.cozy.appdata.xml 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /cozy.doap: -------------------------------------------------------------------------------- 1 | 6 | 7 | Cozy 8 | Listen to audio books 9 | 10 | Python 11 | GTK 4 12 | Libadwaita 13 | 14 | 15 | 16 | Julian Geywitz 17 | 18 | 19 | 20 | geigi 21 | 22 | 23 | 24 | 25 | Benedek Dévényi 26 | 27 | 28 | 29 | rdbende 30 | 31 | 32 | 33 | 34 | 35 | rdbende 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /data/style.css: -------------------------------------------------------------------------------- 1 | .box_hover { 2 | color: @theme_selected_fg_color; 3 | background: @theme_selected_bg_color; 4 | } 5 | 6 | .no_frame { 7 | border-style: none; 8 | } 9 | 10 | .unavailable_box { 11 | background-color: @red_4; 12 | border-radius: 25px; 13 | padding: 3px; 14 | padding-right: 10px; 15 | color: white; 16 | } 17 | 18 | .selected { 19 | color: @theme_selected_fg_color; 20 | background-color: @theme_selected_bg_color; 21 | } 22 | 23 | .book_detail_art { 24 | border-radius: 12px; 25 | } 26 | 27 | .chapter_element { 28 | border-radius: 0.5rem; 29 | } 30 | 31 | .filter-list-box-row { 32 | border-radius: 0.5rem; 33 | } 34 | 35 | .play_button { 36 | background-color: @theme_fg_color; 37 | color: @theme_bg_color; 38 | } 39 | 40 | .player_bar { 41 | background: shade(@theme_bg_color, 0.95) 0%; 42 | } 43 | 44 | .bold { 45 | font-weight: 900; 46 | } 47 | 48 | .semi-bold { 49 | font-weight: 600; 50 | } 51 | 52 | .transparent_bg { 53 | background: transparent; 54 | } 55 | 56 | .failed-import-card { 57 | /* 58 | 6px vertical padding is not ideal, because the text scrolls into 59 | an invisible barrier, but it's nicer when no scrolling is happening 60 | */ 61 | padding: 6px 12px; 62 | } 63 | 64 | .drag-overlay-status-page { 65 | background-color: alpha(@accent_bg_color, 0.65); 66 | color: @accent_fg_color; 67 | } 68 | 69 | .blurred { 70 | filter: blur(6px); 71 | } 72 | 73 | .round-6 { 74 | border-radius: 6px; 75 | } 76 | 77 | .opaque { 78 | opacity: 1; 79 | } 80 | 81 | row.spin.sleep-timer spinbutton text { 82 | opacity: 0; 83 | } 84 | -------------------------------------------------------------------------------- /cozy/view_model/playback_speed_view_model.py: -------------------------------------------------------------------------------- 1 | import inject 2 | 3 | from cozy.architecture.event_sender import EventSender 4 | from cozy.architecture.observable import Observable 5 | from cozy.db.book import Book 6 | from cozy.media.player import Player 7 | 8 | 9 | class PlaybackSpeedViewModel(Observable, EventSender): 10 | _player: Player = inject.attr(Player) 11 | 12 | def __init__(self): 13 | super().__init__() 14 | super(Observable, self).__init__() 15 | 16 | self._book: Book = self._player.loaded_book 17 | 18 | self._player.add_listener(self._on_player_event) 19 | 20 | @property 21 | def playback_speed(self) -> float: 22 | if self._book: 23 | return self._book.playback_speed 24 | 25 | return 1.0 26 | 27 | @playback_speed.setter 28 | def playback_speed(self, new_value: float): 29 | if self._book: 30 | self._book.playback_speed = new_value 31 | self._player.playback_speed = new_value 32 | 33 | def _on_player_event(self, event: str, message): 34 | if event == "chapter-changed" and message: 35 | self._book = message 36 | self._notify("playback_speed") 37 | 38 | def speed_up(self): 39 | self.playback_speed = min(self.playback_speed + 0.1, 3.5) 40 | self._notify("playback_speed") 41 | 42 | def speed_down(self): 43 | self.playback_speed = max(self.playback_speed - 0.1, 0.5) 44 | self._notify("playback_speed") 45 | 46 | def speed_reset(self): 47 | self.playback_speed = 1.0 48 | self._notify("playback_speed") 49 | -------------------------------------------------------------------------------- /cozy/ui/widgets/book_row.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import inject 4 | from gi.repository import Adw, Gtk 5 | 6 | from cozy.control.artwork_cache import ArtworkCache 7 | from cozy.model.book import Book 8 | 9 | BOOK_ICON_SIZE = 52 10 | 11 | 12 | class BookRow(Adw.ActionRow): 13 | _artwork_cache: ArtworkCache = inject.attr(ArtworkCache) 14 | 15 | def __init__( 16 | self, book: Book, on_click: Callable[[Book], None] | None = None 17 | ) -> None: 18 | super().__init__( 19 | title=book.name, subtitle=book.author, selectable=False, use_markup=False 20 | ) 21 | 22 | if on_click is not None: 23 | self.connect("activated", lambda *_: on_click(book)) 24 | self.set_activatable(True) 25 | self.set_tooltip_text(_("Play this book")) 26 | 27 | paintable = self._artwork_cache.get_cover_paintable( 28 | book, self.get_scale_factor(), BOOK_ICON_SIZE 29 | ) 30 | if paintable: 31 | album_art = Gtk.Picture.new_for_paintable(paintable) 32 | album_art.add_css_class("round-6") 33 | album_art.set_overflow(True) 34 | else: 35 | album_art = Gtk.Image.new_from_icon_name("cozy.book-open-symbolic") 36 | album_art.set_pixel_size(BOOK_ICON_SIZE) 37 | 38 | album_art.set_size_request(BOOK_ICON_SIZE, BOOK_ICON_SIZE) 39 | album_art.set_margin_top(6) 40 | album_art.set_margin_bottom(6) 41 | 42 | clamp = Adw.Clamp(maximum_size=BOOK_ICON_SIZE) 43 | clamp.set_child(album_art) 44 | 45 | self.add_prefix(clamp) 46 | -------------------------------------------------------------------------------- /data/ui/search_page.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $SearchView: Adw.Bin { 5 | Stack stack { 6 | Adw.StatusPage start_searching_page { 7 | title: _("Search in Your Library"); 8 | icon-name: 'cozy.library-symbolic'; 9 | } 10 | 11 | Adw.StatusPage nothing_found_page { 12 | title: _("No Results Found"); 13 | icon-name: 'edit-find-symbolic'; 14 | } 15 | 16 | ScrolledWindow search_scroller { 17 | Adw.Clamp { 18 | margin-start: 18; 19 | margin-end: 18; 20 | margin-top: 18; 21 | margin-bottom: 18; 22 | 23 | Box { 24 | orientation: vertical; 25 | spacing: 24; 26 | 27 | Adw.PreferencesGroup book_result_box { 28 | title: _("Book"); 29 | visible: false; 30 | 31 | ListBox book_result_list { 32 | styles [ 33 | "boxed-list", 34 | ] 35 | } 36 | } 37 | 38 | Adw.PreferencesGroup author_result_box { 39 | title: _("Author"); 40 | visible: false; 41 | 42 | ListBox author_result_list { 43 | styles [ 44 | "boxed-list", 45 | ] 46 | } 47 | } 48 | 49 | Adw.PreferencesGroup reader_result_box { 50 | title: _("Reader"); 51 | visible: false; 52 | 53 | ListBox reader_result_list { 54 | styles [ 55 | "boxed-list", 56 | ] 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/flatpak.yml: -------------------------------------------------------------------------------- 1 | name: Flatpak 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths-ignore: 7 | - '**/README.md' 8 | pull_request: 9 | branches: [ "master" ] 10 | 11 | jobs: 12 | flatpak: 13 | runs-on: ubuntu-latest 14 | container: 15 | image: bilelmoussaoui/flatpak-github-actions:gnome-nightly 16 | options: --privileged 17 | 18 | strategy: 19 | matrix: 20 | arch: [x86_64, aarch64] 21 | # Don't fail the whole workflow if one architecture fails 22 | fail-fast: false 23 | 24 | steps: 25 | - uses: actions/checkout@v5 26 | # Docker is required by the docker/setup-qemu-action which enables emulation 27 | - name: Install deps 28 | if: ${{ matrix.arch != 'x86_64' }} 29 | run: | 30 | # Use the static binaries because it's unable to use a package manager 31 | curl https://download.docker.com/linux/static/stable/x86_64/docker-26.0.0.tgz --output ./docker.tgz 32 | tar xzvf docker.tgz 33 | mv docker/* /usr/bin 34 | - name: Set up QEMU 35 | if: ${{ matrix.arch != 'x86_64' }} 36 | id: qemu 37 | uses: docker/setup-qemu-action@v3 38 | with: 39 | platforms: arm64 40 | - uses: flatpak/flatpak-github-actions/flatpak-builder@v6.5 41 | with: 42 | repository-name: gnome-nightly 43 | repository-url: https://nightly.gnome.org/gnome-nightly.flatpakrepo 44 | bundle: com.github.geigi.cozy.flatpak 45 | manifest-path: flatpak/com.github.geigi.cozy.json 46 | cache-key: flatpak-builder-${{ matrix.arch }}-${{ github.sha }} 47 | arch: ${{ matrix.arch }} 48 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.storage-symbolic.svg: -------------------------------------------------------------------------------- 1 | 5 | 13 | 23 | 33 | -------------------------------------------------------------------------------- /cozy/ui/chapter_element.py: -------------------------------------------------------------------------------- 1 | import os 2 | from gi.repository import Adw, GObject, Gtk 3 | from os import path 4 | 5 | from cozy.control.time_format import ns_to_time 6 | from cozy.model.chapter import Chapter 7 | 8 | @Gtk.Template.from_resource("/com/github/geigi/cozy/ui/chapter_element.ui") 9 | class ChapterElement(Adw.ActionRow): 10 | __gtype_name__ = "ChapterElement" 11 | 12 | icon_stack: Gtk.Stack = Gtk.Template.Child() 13 | play_icon: Gtk.Image = Gtk.Template.Child() 14 | number_label: Gtk.Label = Gtk.Template.Child() 15 | duration_label: Gtk.Label = Gtk.Template.Child() 16 | 17 | def __init__(self, chapter: Chapter): 18 | super().__init__() 19 | 20 | self.chapter = chapter 21 | 22 | self.connect("activated", self._on_button_press) 23 | 24 | self.set_title(self.chapter.name) 25 | self.number_label.set_text(str(self.chapter.number)) 26 | 27 | if not os.path.exists(chapter.file): 28 | self.duration_label.set_text(_("File not Found")) 29 | else: 30 | self.duration_label.set_text(ns_to_time(self.chapter.length)) 31 | self.set_tooltip_text(path.basename(self.chapter.file)) 32 | 33 | @GObject.Signal(arg_types=(object,)) 34 | def play_pause_clicked(self, *_): ... 35 | 36 | def _on_button_press(self, *_): 37 | self.emit("play-pause-clicked", self.chapter) 38 | 39 | def select(self): 40 | self.icon_stack.set_visible_child_name("icon") 41 | 42 | def deselect(self): 43 | self.icon_stack.set_visible_child_name("number") 44 | 45 | def set_playing(self, playing): 46 | if playing: 47 | self.play_icon.set_from_icon_name("media-playback-pause-symbolic") 48 | else: 49 | self.play_icon.set_from_icon_name("media-playback-start-symbolic") 50 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | subdir('ui') 2 | subdir('icons') 3 | 4 | gnome.compile_resources( 5 | meson.project_name(), 6 | 'gresource.xml', 7 | dependencies: blueprints, 8 | gresource_bundle: true, 9 | source_dir: meson.current_build_dir(), 10 | install_dir: DATA_DIR, 11 | install: true, 12 | ) 13 | 14 | install_data( 15 | meson.project_name() + '.gschema.xml', 16 | install_dir: join_paths(get_option('datadir'), 'glib-2.0', 'schemas') 17 | ) 18 | 19 | compile_schemas = find_program('glib-compile-schemas', required: false) 20 | if compile_schemas.found() 21 | test('Validate schema file', compile_schemas, 22 | args: ['--strict', '--dry-run', meson.current_source_dir()] 23 | ) 24 | endif 25 | 26 | desktop_file = i18n.merge_file( 27 | input: meson.project_name() + '.desktop', 28 | output: meson.project_name() + '.desktop', 29 | po_dir: join_paths(meson.current_source_dir(), 'po', 'extra'), 30 | type: 'desktop', 31 | install: true, 32 | install_dir: join_paths(get_option('datadir'), 'applications') 33 | ) 34 | 35 | desktop_utils = find_program('desktop-file-validate', required: false) 36 | if desktop_utils.found() 37 | test('Validate desktop file', desktop_utils, 38 | args: [desktop_file] 39 | ) 40 | endif 41 | 42 | appstream_file = i18n.merge_file( 43 | input: meson.project_name() + '.appdata.xml', 44 | output: meson.project_name() + '.appdata.xml', 45 | po_dir: join_paths(meson.current_source_dir(), 'po', 'extra'), 46 | install: true, 47 | install_dir: join_paths(get_option('datadir'), 'metainfo') 48 | ) 49 | 50 | appstreamcli = find_program('appstreamcli', required: false) 51 | if appstreamcli.found() 52 | test('Validate appstream file', appstreamcli, 53 | args: ['validate', '--no-net', '--explain', appstream_file], 54 | workdir: meson.current_build_dir() 55 | ) 56 | endif 57 | -------------------------------------------------------------------------------- /cozy/ui/import_failed_dialog.py: -------------------------------------------------------------------------------- 1 | from gettext import gettext as _ 2 | 3 | import inject 4 | from gi.repository import Adw, Gtk 5 | 6 | HEADER = _("This can have multiple reasons:") 7 | POSSIBILITIES = "\n • ".join(( # yes, it is a hack, because \t would be too wide 8 | "", 9 | _("The audio format is not supported"), 10 | _("The path or filename contains non utf-8 characters"), 11 | _("The file(s) are no valid audio files"), 12 | _("The file(s) are corrupt"), 13 | )) 14 | 15 | message = HEADER + POSSIBILITIES 16 | 17 | 18 | class ImportFailedDialog(Adw.AlertDialog): 19 | """ 20 | Dialog that displays failed files on import. 21 | """ 22 | main_window = inject.attr("MainWindow") 23 | 24 | def __init__(self, files: list[str]): 25 | super().__init__( 26 | heading=_("Some files could not be imported"), 27 | default_response="cancel", 28 | close_response="cancel", 29 | ) 30 | 31 | self.add_response("cancel", _("Ok")) 32 | 33 | box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=18) 34 | body_label = Gtk.Label(label=message) 35 | 36 | text_buffer = Gtk.TextBuffer( 37 | text="\n".join(files).encode("utf-8", errors="replace").decode("utf-8") 38 | ) 39 | text_view = Gtk.TextView( 40 | buffer=text_buffer, 41 | editable=False, 42 | cursor_visible=False, 43 | css_classes=["card", "failed-import-card", "monospace"] 44 | ) 45 | 46 | scroller = Gtk.ScrolledWindow( 47 | max_content_height=200, 48 | propagate_natural_height=True, 49 | child=text_view 50 | ) 51 | 52 | box.append(body_label) 53 | box.append(scroller) 54 | self.set_extra_child(box) 55 | 56 | def present(self) -> None: 57 | super().present(self.main_window.window) 58 | 59 | -------------------------------------------------------------------------------- /cozy/architecture/observable.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Callable 3 | 4 | from gi.repository import GLib 5 | 6 | from cozy.report import reporter 7 | 8 | log = logging.getLogger("observable") 9 | 10 | 11 | class Observable: 12 | def __init__(self): 13 | self._observers = {} 14 | 15 | def bind_to(self, prop: str, callback: Callable): 16 | if prop in self._observers: 17 | self._observers[prop].append(callback) 18 | else: 19 | self._observers[prop] = [callback] 20 | 21 | def remove_bind(self, prop: str, callback: Callable): 22 | if not prop: 23 | log.error("Cannot remove bind for empty prop.") 24 | reporter.error("observable", "Cannot remove bind for empty prop.") 25 | return 26 | 27 | if not callback: 28 | log.error("Cannot remove bind for empty callback.") 29 | reporter.error("observable", "Cannot remove bind for empty callback.") 30 | return 31 | 32 | if prop in self._observers: 33 | if callback in self._observers[prop]: 34 | self._observers[prop].remove(callback) 35 | else: 36 | log.info("Callback not found in prop's %s observers. Skipping remove bind...", prop) 37 | else: 38 | log.info("Prop not found in observers. Skipping remove bind...") 39 | 40 | def _notify(self, prop: str): 41 | if prop not in self._observers: 42 | return 43 | 44 | try: 45 | for callback in self._observers[prop]: 46 | callback() 47 | except Exception as e: 48 | log.error(e) 49 | reporter.exception("observable", e) 50 | 51 | def _notify_main_thread(self, prop: str): 52 | GLib.MainContext.default().invoke_full(GLib.PRIORITY_DEFAULT_IDLE, self._notify, prop) 53 | 54 | def _destroy_observers(self): 55 | self._observers = {} 56 | -------------------------------------------------------------------------------- /.github/workflows/flathub.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on Flathub 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | #on: 8 | # watch: 9 | # types: [started] 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - name: Install dependencies 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get install libnode-dev node-gyp libssl-dev 23 | sudo apt-get install npm 24 | sudo npm install -g json 25 | 26 | - name: Download latest cozy release 27 | run: | 28 | curl -s https://api.github.com/repos/geigi/cozy/releases/latest | json tag_name > /tmp/VERSION 29 | echo https://github.com/geigi/cozy/archive/$(cat /tmp/VERSION).tar.gz > /tmp/RELEASE_URL 30 | wget -O /tmp/cozy.tar.gz $(cat /tmp/RELEASE_URL) 31 | sha256sum /tmp/cozy.tar.gz | cut -d " " -f 1 > /tmp/SHA256SUM 32 | 33 | - name: Clone Flathub repository 34 | run: git clone https://geigi:${{ secrets.FLATHUB_TOKEN }}@github.com/flathub/com.github.geigi.cozy.git /tmp/flathub 35 | 36 | - name: Update Flathub json 37 | run: | 38 | ls /tmp/flathub 39 | json -I -f /tmp/flathub/com.github.geigi.cozy.json -e "this.modules[this.modules.length - 1].sources[0].url='https://github.com/geigi/cozy/archive/$(cat /tmp/VERSION).tar.gz'" 40 | json -I -f /tmp/flathub/com.github.geigi.cozy.json -e "this.modules[this.modules.length - 1].sources[0].sha256='$(cat /tmp/SHA256SUM)'" 41 | 42 | - name: Push changes 43 | run: | 44 | git config --global user.email "github@geigi.de" 45 | git config --global user.name "Github Actions" 46 | cd /tmp/flathub 47 | git commit -am "Bump version to $(cat /tmp/VERSION)" 48 | git push 49 | 50 | - name: Wait for flathub build to complete 51 | run: | 52 | cd $GITHUB_WORKSPACE/.ci 53 | chmod +x *.sh 54 | ./flathub_wait_for_build.sh 55 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.bed-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /cozy/ui/about_window.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from gi.repository import Adw, Gio, Gtk 4 | 5 | 6 | class AboutWindow: 7 | def __init__(self, version: str) -> None: 8 | self._window = Adw.AboutDialog.new_from_appdata( 9 | "/com/github/geigi/cozy/appdata/com.github.geigi.cozy.appdata.xml", 10 | release_notes_version=version, 11 | ) 12 | 13 | contributors = self.get_contributors() 14 | self._window.set_developers(sorted(contributors["code"])) 15 | self._window.set_designers(sorted(contributors["design"])) 16 | self._window.set_artists(sorted(contributors["icon"])) 17 | 18 | self._window.set_license_type(Gtk.License.GPL_3_0) 19 | 20 | # Translators: Replace "translator-credits" with your names, one name per line 21 | self._window.set_translator_credits(_("translator-credits")) 22 | 23 | self.set_extra_credits() 24 | 25 | self.connect = self._window.connect 26 | 27 | def get_contributors(self) -> list[str]: 28 | authors_file = Gio.resources_lookup_data( 29 | "/com/github/geigi/cozy/appdata/authors.list", Gio.ResourceLookupFlags.NONE 30 | ) 31 | 32 | current_section = "" 33 | result = defaultdict(list) 34 | for line in authors_file.get_data().decode().splitlines(): 35 | if line.startswith("#"): 36 | current_section = line[1:].strip().lower() 37 | elif line.startswith("-"): 38 | result[current_section].append(line[1:].strip()) 39 | 40 | return result 41 | 42 | def set_extra_credits(self) -> None: 43 | self._window.add_acknowledgement_section( 44 | _("Patreon Supporters"), 45 | ["Fred Warren", "Gabriel", "Hu Mann", "Josiah", "Oleksii Kriukov"], 46 | ) 47 | self._window.add_acknowledgement_section(_("m4b chapter support in mutagen"), ("mweinelt",)) 48 | 49 | def present(self, parent: Adw.ApplicationWindow) -> None: 50 | self._window.present(parent) 51 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.settings-symbolic.svg: -------------------------------------------------------------------------------- 1 | 5 | 13 | -------------------------------------------------------------------------------- /cozy/model/storage.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from peewee import SqliteDatabase 4 | 5 | from cozy.db.storage import Storage as StorageModel 6 | 7 | 8 | class InvalidPath(Exception): 9 | pass 10 | 11 | 12 | class Storage: 13 | def __init__(self, db: SqliteDatabase, db_id: int): 14 | self._db: SqliteDatabase = db 15 | self.id: int = db_id 16 | 17 | self._get_db_object() 18 | 19 | @staticmethod 20 | def new(db: SqliteDatabase, path: str): 21 | db_obj = StorageModel.create(path=path) 22 | return Storage(db, db_obj.id) 23 | 24 | def _get_db_object(self): 25 | self._db_object: StorageModel = StorageModel.get(self.id) 26 | 27 | @property 28 | def db_object(self): 29 | return self._db_object 30 | 31 | @property 32 | def path(self): 33 | return self._db_object.path 34 | 35 | @path.setter 36 | def path(self, path: str): 37 | if not Path(path).is_absolute(): 38 | raise InvalidPath 39 | 40 | self._db_object.path = path 41 | self._db_object.save(only=self._db_object.dirty_fields) 42 | 43 | @property 44 | def location_type(self): 45 | return self._db_object.location_type 46 | 47 | @location_type.setter 48 | def location_type(self, new_location_type: int): 49 | self._db_object.location_type = new_location_type 50 | self._db_object.save(only=self._db_object.dirty_fields) 51 | 52 | @property 53 | def default(self): 54 | return self._db_object.default 55 | 56 | @default.setter 57 | def default(self, new_default: bool): 58 | self._db_object.default = new_default 59 | self._db_object.save(only=self._db_object.dirty_fields) 60 | 61 | @property 62 | def external(self): 63 | return self._db_object.external 64 | 65 | @external.setter 66 | def external(self, new_external: bool): 67 | self._db_object.external = new_external 68 | self._db_object.save(only=self._db_object.dirty_fields) 69 | 70 | def delete(self): 71 | self._db_object.delete_instance(recursive=True, delete_nullable=False) 72 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.reader-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Code 2 | - Julian Geywitz 3 | - Benedek Dévényi 4 | - A6GibKm 5 | - alyssais 6 | - apandada1 7 | - AsavarTzeth 8 | - Fatih20 9 | - NathanBnm 10 | - camellan 11 | - chris-kobrzak 12 | - elya5 13 | - foliva 14 | - grenade 15 | - jnbr 16 | - jubalh 17 | - kaphula 18 | - leuc 19 | - magnickolas 20 | - meisenzahl 21 | - naglis 22 | - oleg-krv 23 | - paper42 24 | - phpwutz 25 | - rapenne-s 26 | - thibaultamartin 27 | - umeboshi2 28 | - worldofpeace 29 | 30 | # Design 31 | - Julian Geywitz 32 | - Tobias Bernard 33 | - Benedek Dévényi 34 | 35 | # Icon 36 | - Jakub Steiner 37 | 38 | # Translators 39 | ``` 40 | Ainte 41 | AndreBarata 42 | Andrey389 43 | Asyx 44 | BunColak 45 | Caarmi 46 | CiTyBear 47 | Distil62 48 | Fitoschido 49 | Floflr 50 | Foxyriot 51 | HansCz 52 | IngrownMink4 53 | IvoIliev 54 | Jagadeeshvarma 55 | Kwentin 56 | MageJohn 57 | NHiX 58 | Nimmerliefde 59 | Oi_Suomi_On 60 | Okton 61 | Panwar108 62 | Potty0 63 | Sebosun 64 | TheMBTH 65 | TheRuleOfMike 66 | Vistaus 67 | W2hJ3MOmIRovEpTeahe80jC 68 | WhiredPlanck 69 | _caasi 70 | aKodi 71 | abcmen 72 | abuyop 73 | albanobattistella 74 | amadeussss 75 | andreapillai 76 | arejano 77 | artnay 78 | b3nj4m1n 79 | baschdl78 80 | camellan 81 | cavinsmith 82 | cho2 83 | chris109b 84 | cjfloss 85 | ckaotik 86 | corentinbettiol 87 | dtgoitia 88 | dzerus3 89 | elgosz 90 | endiamesos 91 | eson 92 | fishcake13 93 | fountain 94 | fran.dieguez 95 | georgelemental 96 | giuscasula 97 | goran.p1123581321 98 | hamidout 99 | hkoivuneva 100 | jan.sundman 101 | jan_nekvasil 102 | jouselt 103 | karaagac 104 | kevinmueller 105 | leondorus 106 | libreajans 107 | linuxmasterclub 108 | magnickolas 109 | makaleks 110 | mannycalavera42 111 | mardojai 112 | markluethje 113 | milotype 114 | mvainola 115 | n1k7as 116 | nikkpark 117 | no404error 118 | nvivant 119 | oleg_krv 120 | ooverloord 121 | oscfdezdz 122 | pavelz 123 | rafaelff1 124 | ragouel 125 | saarikko 126 | sobeitnow0 127 | sojuz151 128 | steno 129 | tclokie 130 | test21 131 | thibaultmartin 132 | translatornator 133 | tsitokhtsev 134 | twardowskidev 135 | txelu 136 | useruseruser1233211 137 | vanhoxx 138 | vlabo 139 | xfgusta 140 | yalexaner 141 | ``` 142 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.no-bed-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/opensuse.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on OBS (OpenSuse) 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | #on: 8 | # watch: 9 | # types: [started] 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - name: Install dependencies 20 | run: | 21 | mkdir -p ~/.config/osc 22 | sudo apt-get update 23 | sudo apt-get install libnode-dev node-gyp libssl-dev 24 | sudo apt-get install osc npm python3-m2crypto 25 | sudo npm install -g json 26 | 27 | - name: Setup osc login 28 | run: | 29 | cat >~/.config/osc/oscrc < /tmp/VERSION 39 | curl -s https://api.github.com/repos/geigi/cozy/releases/latest | json body > /tmp/CHANGES 40 | echo https://github.com/geigi/cozy/archive/$(cat /tmp/VERSION).tar.gz > /tmp/RELEASE_URL 41 | cd X11:Pantheon:Apps/cozy 42 | rm cozy-*.tar.gz 43 | wget -O cozy-$(cat /tmp/VERSION).tar.gz $(cat /tmp/RELEASE_URL) 44 | 45 | - name: Update rpm info files 46 | run: | 47 | cd X11:Pantheon:Apps/cozy 48 | sed -i -e 's/- / * /g' /tmp/CHANGES 49 | sed -i "1s/^/Update to $(cat /tmp/VERSION)\n/" /tmp/CHANGES 50 | osc vc -m "$(cat /tmp/CHANGES)" 51 | sed -i -e "s/Version:.*/Version: $(cat /tmp/VERSION)/g" cozy.spec 52 | 53 | - name: Osc check-in 54 | run: | 55 | cd X11:Pantheon:Apps/cozy 56 | osc addremove 57 | osc ci --noservice -m "Update to version $(cat /tmp/VERSION)." 58 | 59 | - name: Wait for obs build to complete 60 | run: | 61 | chmod +x $GITHUB_WORKSPACE/.ci/*.sh 62 | cd X11:Pantheon:Apps/cozy 63 | $GITHUB_WORKSPACE/.ci/obs_wait_for_build.sh 64 | 65 | - name: Submit package 66 | run: | 67 | cd X11:Pantheon:Apps/cozy 68 | osc submitrequest -m "Update to version $(cat /tmp/VERSION)." 69 | -------------------------------------------------------------------------------- /data/ui/preferences.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | 5 | Adjustment forward_duration_adjustment { 6 | lower: 5; 7 | upper: 120; 8 | step-increment: 5; 9 | page-increment: 10; 10 | } 11 | 12 | Adjustment rewind_duration_adjustment { 13 | lower: 5; 14 | upper: 120; 15 | value: 15; 16 | step-increment: 5; 17 | page-increment: 10; 18 | } 19 | 20 | template $PreferencesWindow: Adw.PreferencesDialog { 21 | Adw.PreferencesPage { 22 | icon-name: 'cozy.settings-symbolic'; 23 | title: _("General"); 24 | 25 | Adw.PreferencesGroup { 26 | title: _("Tags"); 27 | 28 | Adw.SwitchRow swap_author_reader_switch { 29 | title: _("Swap Author and Reader"); 30 | subtitle: _("Activate if author and reader are displayed the wrong way"); 31 | } 32 | } 33 | 34 | Adw.PreferencesGroup { 35 | title: _("Playback"); 36 | 37 | Adw.SwitchRow replay_switch { 38 | title: _("Replay"); 39 | subtitle: _("Rewind 30 seconds of the current book when starting Cozy"); 40 | } 41 | 42 | Adw.SpinRow rewind_duration_spin_button { 43 | title: _("Rewind Duration"); 44 | focusable: true; 45 | adjustment: rewind_duration_adjustment; 46 | snap-to-ticks: true; 47 | numeric: true; 48 | } 49 | 50 | Adw.SpinRow forward_duration_spin_button { 51 | title: _("Forward Duration"); 52 | focusable: true; 53 | adjustment: forward_duration_adjustment; 54 | snap-to-ticks: true; 55 | numeric: true; 56 | } 57 | } 58 | } 59 | 60 | Adw.PreferencesPage storages_page { 61 | icon-name: 'cozy.storage-symbolic'; 62 | title: _("Storage"); 63 | 64 | Adw.PreferencesGroup { 65 | title: _("Artwork"); 66 | 67 | Adw.SwitchRow artwork_prefer_external_switch { 68 | title: _("Prefer External Images Over Embedded Cover"); 69 | subtitle: _("Always use images (cover.jpg, *.png, …) when available"); 70 | } 71 | } 72 | } 73 | 74 | Adw.PreferencesPage { 75 | icon-name: 'cozy.feedback-symbolic'; 76 | title: _("Feedback"); 77 | 78 | Adw.PreferencesGroup user_feedback_preference_group { 79 | title: _("Error Reporting"); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /cozy/model/chapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from cozy.architecture.event_sender import EventSender 4 | 5 | 6 | class Chapter(ABC, EventSender): 7 | id: int 8 | 9 | def __init__(self): 10 | super().__init__() 11 | super(ABC, self).__init__() 12 | 13 | @property 14 | @abstractmethod 15 | def name(self) -> str: 16 | pass 17 | 18 | @name.setter 19 | @abstractmethod 20 | def name(self, new_name: str): 21 | pass 22 | 23 | @property 24 | @abstractmethod 25 | def number(self) -> int: 26 | pass 27 | 28 | @number.setter 29 | @abstractmethod 30 | def number(self, new_number: int): 31 | pass 32 | 33 | @property 34 | @abstractmethod 35 | def disk(self) -> int: 36 | pass 37 | 38 | @disk.setter 39 | @abstractmethod 40 | def disk(self, new_disk: int): 41 | pass 42 | 43 | @property 44 | @abstractmethod 45 | def position(self) -> int: 46 | pass 47 | 48 | @position.setter 49 | @abstractmethod 50 | def position(self, new_position: int): 51 | pass 52 | 53 | @property 54 | @abstractmethod 55 | def file(self) -> str: 56 | pass 57 | 58 | @file.setter 59 | @abstractmethod 60 | def file(self, new_file: str): 61 | pass 62 | 63 | @property 64 | @abstractmethod 65 | def file_id(self) -> int: 66 | pass 67 | 68 | @property 69 | @abstractmethod 70 | def length(self) -> float: 71 | pass 72 | 73 | @length.setter 74 | @abstractmethod 75 | def length(self, new_length: float): 76 | pass 77 | 78 | @property 79 | @abstractmethod 80 | def modified(self) -> int: 81 | pass 82 | 83 | @modified.setter 84 | @abstractmethod 85 | def modified(self, new_modified: int): 86 | pass 87 | 88 | @property 89 | @abstractmethod 90 | def start_position(self) -> int: 91 | pass 92 | 93 | @property 94 | @abstractmethod 95 | def end_position(self) -> int: 96 | pass 97 | 98 | @abstractmethod 99 | def delete(self): 100 | pass 101 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.search-large-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/aur.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on AUR 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | #on: 8 | # watch: 9 | # types: [started] 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - name: Install dependencies 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get install libnode-dev node-gyp libssl-dev 23 | sudo apt-get install npm 24 | sudo npm install -g json 25 | 26 | - name: Download latest cozy release 27 | run: | 28 | curl -s https://api.github.com/repos/geigi/cozy/releases/latest | json tag_name > /tmp/VERSION 29 | echo https://github.com/geigi/cozy/archive/$(cat /tmp/VERSION).tar.gz > /tmp/RELEASE_URL 30 | wget -O /tmp/cozy.tar.gz $(cat /tmp/RELEASE_URL) 31 | sha256sum /tmp/cozy.tar.gz | cut -d " " -f 1 > /tmp/SHA256SUM 32 | 33 | - name: Setup SSH Keys and known_hosts 34 | env: 35 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 36 | run: | 37 | mkdir -p ~/.ssh 38 | ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts 39 | ssh-agent -a $SSH_AUTH_SOCK > /dev/null 40 | ssh-add - <<< "${{ secrets.AUR_PRIVATE }}" 41 | 42 | - name: Clone cozy-audiobooks repository 43 | env: 44 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 45 | run: git clone ssh://aur@aur.archlinux.org/cozy-audiobooks.git /tmp/aur 46 | 47 | - name: Update PKGBUILD 48 | run: | 49 | ls /tmp/aur 50 | cd /tmp/aur 51 | sed -i "s/^pkgver.*\$/pkgver=$(cat /tmp/VERSION)/" PKGBUILD 52 | sed -i "s/^sha256sum.*\$/sha256sums=('$(cat /tmp/SHA256SUM)')/" PKGBUILD 53 | sed -i "s/.*pkgver.*\$/ pkgver = $(cat /tmp/VERSION)/" .SRCINFO 54 | sed -i "s/.*source.*\$/ source = https\:\/\/github.com\/geigi\/cozy\/archive\/$(cat /tmp/VERSION)\.tar\.gz/" .SRCINFO 55 | sed -i "s/.*sha256sums.*\$/ sha256sums = $(cat /tmp/SHA256SUM)/" .SRCINFO 56 | 57 | - name: Push changes 58 | env: 59 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 60 | run: | 61 | git config --global user.email "github@geigi.de" 62 | git config --global user.name "Github Actions" 63 | cd /tmp/aur 64 | git commit -am "Bump version to $(cat /tmp/VERSION)" 65 | git push 66 | -------------------------------------------------------------------------------- /cozy/ui/preferences_window.py: -------------------------------------------------------------------------------- 1 | import inject 2 | from gi.repository import Adw, Gio, Gtk 3 | 4 | from cozy.ui.widgets.error_reporting import ErrorReporting 5 | from cozy.ui.widgets.storages import StorageLocations 6 | from cozy.view_model.settings_view_model import SettingsViewModel 7 | 8 | 9 | @Gtk.Template.from_resource("/com/github/geigi/cozy/ui/preferences.ui") 10 | class PreferencesWindow(Adw.PreferencesDialog): 11 | __gtype_name__ = "PreferencesWindow" 12 | 13 | _glib_settings: Gio.Settings = inject.attr(Gio.Settings) 14 | _view_model: SettingsViewModel = inject.attr(SettingsViewModel) 15 | 16 | storages_page: Adw.PreferencesPage = Gtk.Template.Child() 17 | user_feedback_preference_group: Adw.PreferencesGroup = Gtk.Template.Child() 18 | 19 | swap_author_reader_switch: Adw.SwitchRow = Gtk.Template.Child() 20 | replay_switch: Adw.SwitchRow = Gtk.Template.Child() 21 | artwork_prefer_external_switch: Adw.SwitchRow = Gtk.Template.Child() 22 | 23 | rewind_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() 24 | forward_duration_adjustment: Gtk.Adjustment = Gtk.Template.Child() 25 | 26 | def __init__(self) -> None: 27 | super().__init__() 28 | 29 | error_reporting = ErrorReporting() 30 | self.user_feedback_preference_group.add(error_reporting) 31 | 32 | self.storage_locations_view = StorageLocations() 33 | self.storages_page.add(self.storage_locations_view) 34 | 35 | self._view_model.bind_to("lock_ui", self._on_lock_ui_changed) 36 | self._bind_settings() 37 | 38 | def _bind_settings(self) -> None: 39 | bind_settings = lambda setting, widget, property: self._glib_settings.bind( 40 | setting, widget, property, Gio.SettingsBindFlags.DEFAULT 41 | ) 42 | 43 | bind_settings("swap-author-reader", self.swap_author_reader_switch, "active") 44 | bind_settings("replay", self.replay_switch, "active") 45 | bind_settings("rewind-duration", self.rewind_duration_adjustment, "value") 46 | bind_settings("forward-duration", self.forward_duration_adjustment, "value") 47 | bind_settings("prefer-external-cover", self.artwork_prefer_external_switch, "active") 48 | 49 | def _on_lock_ui_changed(self) -> None: 50 | self.storage_locations_view.set_sensitive(not self._view_model.lock_ui) 51 | 52 | def present(self, parent: Adw.ApplicationWindow) -> None: 53 | super().present(parent) 54 | -------------------------------------------------------------------------------- /test/books.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Test Book", 5 | "author": "Test Author", 6 | "reader": "Test Reader", 7 | "position": 0, 8 | "rating": 0 9 | }, 10 | { 11 | "id": 2, 12 | "name": "Harry Potter and the Philosophers Stone", 13 | "author": "J. K. Rowling", 14 | "reader": "Stephen Fry", 15 | "position": 0, 16 | "rating": 0 17 | }, 18 | { 19 | "id": 3, 20 | "name": "Harry Potter and the Order of the Phoenix", 21 | "author": "J. K. Rowling", 22 | "reader": "Stephen Fry", 23 | "position": 0, 24 | "rating": 0 25 | }, 26 | { 27 | "id": 4, 28 | "name": "Harry Potter And The Deathly Hallows", 29 | "author": "J. K. Rowling", 30 | "reader": "Stephen Fry", 31 | "position": 0, 32 | "rating": 0 33 | }, 34 | { 35 | "id": 5, 36 | "name": "Harry Potter and the Chamber of Secrets", 37 | "author": "J. K. Rowling", 38 | "reader": "Stephen Fry", 39 | "position": 0, 40 | "rating": 0 41 | }, 42 | { 43 | "id": 6, 44 | "name": "Harry Potter and the Prisoner of Azkaban", 45 | "author": "J. K. Rowling", 46 | "reader": "Stephen Fry", 47 | "position": 0, 48 | "rating": 0 49 | }, 50 | { 51 | "id": 7, 52 | "name": "Harry Potter and the Goblet of Fire", 53 | "author": "J. K. Rowling", 54 | "reader": "Stephen Fry", 55 | "position": 0, 56 | "rating": 0 57 | }, 58 | { 59 | "id": 8, 60 | "name": "Harry Potter and the Half-Blood Prince", 61 | "author": "J. K. Rowling", 62 | "reader": "Stephen Fry", 63 | "position": 0, 64 | "rating": 0 65 | }, 66 | { 67 | "id": 9, 68 | "name": "20.000 Meilen unter den Meeren", 69 | "author": "Jules Verne", 70 | "reader": "Ernst Jacobi, Gottfried John, Hermann Lause, Peter Gavajda (Sprecher)", 71 | "position": 222, 72 | "rating": 0 73 | }, 74 | { 75 | "id": 10, 76 | "name": "This is a book without tracks", 77 | "author": "Test", 78 | "reader": "Test", 79 | "position": 0, 80 | "rating": 0 81 | }, 82 | { 83 | "id": 11, 84 | "name": "This is a book with multiple chapters in a single file", 85 | "author": "Test", 86 | "reader": "Test", 87 | "position": 0, 88 | "rating": 0 89 | }, 90 | { 91 | "id": 12, 92 | "name": "This is a book no chapter names", 93 | "author": "Test", 94 | "reader": "Test", 95 | "position": 0, 96 | "rating": 0 97 | } 98 | ] -------------------------------------------------------------------------------- /cozy/ui/widgets/error_reporting.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | import inject 4 | from gi.repository import Adw, Gtk 5 | 6 | from cozy.settings import ApplicationSettings 7 | 8 | LEVELS = [ 9 | _("Disabled"), 10 | _("Basic error reporting"), 11 | _("Detailed error reporting"), 12 | _("Detailed, with media types"), 13 | ] 14 | 15 | LEVEL_DESCRIPTION = [ 16 | _("No error or crash reporting."), 17 | _("The following information will be sent in case of an error or crash:"), 18 | ] 19 | 20 | LEVEL_DETAILS = [ 21 | [], 22 | [ 23 | _("Which type of error occurred"), 24 | _("Line of code where an error occurred"), 25 | _("Cozy's version"), 26 | ], 27 | [_("Linux distribution"), _("Desktop environment")], 28 | [_("Media type of files that Cozy couldn't import")], 29 | ] 30 | 31 | 32 | @Gtk.Template.from_resource("/com/github/geigi/cozy/ui/error_reporting.ui") 33 | class ErrorReporting(Gtk.Box): 34 | __gtype_name__ = "ErrorReporting" 35 | 36 | description: Adw.ActionRow = Gtk.Template.Child() 37 | detail_combo: Adw.ComboRow = Gtk.Template.Child() 38 | 39 | app_settings: ApplicationSettings = inject.attr(ApplicationSettings) 40 | 41 | def __init__(self, **kwargs): 42 | super().__init__(**kwargs) 43 | 44 | levels_list = Gtk.StringList(strings=LEVELS) 45 | self.detail_combo.props.model = levels_list 46 | self.detail_combo.connect("notify::selected-item", self._level_selected) 47 | 48 | self._load_report_level() 49 | 50 | self.app_settings.add_listener(self._on_app_setting_changed) 51 | 52 | def _load_report_level(self): 53 | level = self.app_settings.report_level 54 | self._update_description(level) 55 | self.detail_combo.set_selected(level) 56 | 57 | def _level_selected(self, obj, param) -> None: 58 | selected = obj.get_property(param.name).get_string() 59 | index = LEVELS.index(selected) 60 | self.app_settings.report_level = index 61 | self._update_description(index) 62 | 63 | def _update_description(self, level: int): 64 | self.description.set_title(LEVEL_DESCRIPTION[min(level, 1)]) 65 | details = "\n".join(["• " + i for i in chain(*LEVEL_DETAILS[: level + 1])]) 66 | self.description.set_subtitle(details) 67 | 68 | def _on_app_setting_changed(self, event, _): 69 | if event == "report-level": 70 | self._load_report_level() 71 | -------------------------------------------------------------------------------- /cozy/application.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import platform 3 | import sys 4 | import threading 5 | from traceback import format_exception 6 | 7 | import distro 8 | from gi.repository import Adw, GLib 9 | 10 | from cozy import __version__ 11 | from cozy.app_controller import AppController 12 | from cozy.control.db import init_db 13 | from cozy.control.mpris import MPRIS 14 | from cozy.report import reporter 15 | from cozy.ui.main_view import CozyUI 16 | from cozy.ui.widgets.filter_list_box import FilterListBox 17 | 18 | log = logging.getLogger("application") 19 | 20 | 21 | class Application(Adw.Application): 22 | ui: CozyUI 23 | app_controller: AppController 24 | 25 | def __init__(self, pkgdatadir: str): 26 | self.pkgdatadir = pkgdatadir 27 | 28 | super().__init__(application_id="com.github.geigi.cozy") 29 | self.init_custom_widgets() 30 | 31 | GLib.setenv("PULSE_PROP_media.role", "music", True) 32 | GLib.set_application_name("Cozy") 33 | 34 | threading.excepthook = self.handle_exception 35 | 36 | def do_startup(self): 37 | log.info(distro.linux_distribution(full_distribution_name=False)) 38 | log.info("Starting up cozy %s", __version__) 39 | log.info("libadwaita version: %s", Adw._version) 40 | 41 | self.ui = CozyUI(self, __version__) 42 | Adw.Application.do_startup(self) 43 | init_db() 44 | self.ui.startup() 45 | 46 | def do_activate(self): 47 | main_window_builder = self.ui.get_builder() 48 | self.app_controller = AppController(self, main_window_builder, self.ui) 49 | 50 | self.ui.activate(self.app_controller.library_view) 51 | self.add_window(self.ui.window) 52 | 53 | if platform.system().lower() == "linux": 54 | mpris = MPRIS(self) 55 | mpris._on_current_changed() 56 | 57 | def handle_exception(self, _): 58 | print("handle exception") 59 | 60 | exc_type, exc_value, exc_traceback = sys.exc_info() 61 | 62 | if exc_type is SystemExit: 63 | return 64 | 65 | try: 66 | reporter.exception( 67 | "uncaught", 68 | exc_value, 69 | "\n".join(format_exception(exc_type, exc_value, exc_traceback)), 70 | ) 71 | finally: 72 | sys.excepthook(exc_type, exc_value, exc_traceback) 73 | 74 | def quit(self): 75 | super().quit() 76 | 77 | @staticmethod 78 | def init_custom_widgets(): 79 | FilterListBox() 80 | -------------------------------------------------------------------------------- /data/ui/headerbar.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $Headerbar: Box { 5 | orientation: vertical; 6 | 7 | Adw.HeaderBar headerbar { 8 | [title] 9 | Adw.ViewSwitcher view_switcher { 10 | policy: wide; 11 | } 12 | 13 | [start] 14 | ToggleButton show_sidebar_button { 15 | visible: false; 16 | icon-name: 'sidebar-show-symbolic'; 17 | tooltip-text: _("Toggle Filter Sidebar"); 18 | } 19 | 20 | [end] 21 | MenuButton menu_button { 22 | tooltip-text: _("Options"); 23 | menu-model: primary_menu; 24 | icon-name: 'open-menu-symbolic'; 25 | primary: true; 26 | 27 | accessibility { 28 | label: _("Open the options popover"); 29 | } 30 | } 31 | 32 | [end] 33 | ToggleButton search_button { 34 | name: 'Search toggle button'; 35 | tooltip-text: _("Search your library"); 36 | icon-name: 'cozy.search-large-symbolic'; 37 | 38 | accessibility { 39 | label: _("Open the search popover"); 40 | } 41 | } 42 | 43 | [end] 44 | MenuButton progress_menu_button { 45 | visible: false; 46 | can-focus: true; 47 | tooltip-text: _("Display background task progress"); 48 | 49 | Adw.Spinner progress_spinner {} 50 | 51 | styles [ 52 | "flat", 53 | ] 54 | } 55 | } 56 | 57 | SearchBar search_bar { 58 | search-mode-enabled: bind search_button.active bidirectional; 59 | 60 | Adw.Clamp { 61 | margin-start: 6; 62 | margin-end: 6; 63 | margin-bottom: 6; 64 | maximum-size: 450; 65 | 66 | SearchEntry search_entry { 67 | search-delay: 100; 68 | placeholder-text: _("Start typing..."); 69 | hexpand: true; 70 | } 71 | } 72 | } 73 | } 74 | 75 | menu primary_menu { 76 | section { 77 | item { 78 | action: 'app.scan'; 79 | label: _("_Scan Library"); 80 | } 81 | } 82 | 83 | section { 84 | item { 85 | action: 'app.hide_offline'; 86 | label: _("_Hide unavailable books"); 87 | } 88 | } 89 | 90 | section { 91 | item { 92 | action: 'app.prefs'; 93 | label: _("_Preferences"); 94 | } 95 | 96 | item { 97 | action: 'app.about'; 98 | label: _("_About Cozy"); 99 | } 100 | } 101 | 102 | section { 103 | item { 104 | action: 'app.quit'; 105 | label: _("_Quit"); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /cozy/ui/file_not_found_dialog.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import inject 4 | from gi.repository import Adw, Gio, GLib, Gtk 5 | 6 | from cozy.media.importer import Importer 7 | from cozy.model.chapter import Chapter 8 | 9 | 10 | class FileNotFoundDialog(Adw.AlertDialog): 11 | main_window = inject.attr("MainWindow") 12 | _importer: Importer = inject.attr(Importer) 13 | 14 | def __init__(self, chapter: Chapter): 15 | self.missing_chapter = chapter 16 | 17 | super().__init__( 18 | heading=_("File not Found"), 19 | body=_("This file could not be found. Do you want to locate it manually?"), 20 | default_response="locate", 21 | close_response="cancel", 22 | ) 23 | 24 | self.add_response("cancel", _("Cancel")) 25 | self.add_response("locate", _("Locate")) 26 | self.set_response_appearance("locate", Adw.ResponseAppearance.SUGGESTED) 27 | 28 | label = Gtk.Label(label=chapter.file, margin_top=12, wrap=True) 29 | label.add_css_class("monospace") 30 | self.set_extra_child(label) 31 | 32 | self.connect("response", self._on_locate) 33 | 34 | def _on_locate(self, __, response): 35 | if response == "locate": 36 | file_dialog = Gtk.FileDialog(title=_("Locate Missing File")) 37 | 38 | extension = Path(self.missing_chapter.file).suffix[1:] 39 | current_extension_filter = Gtk.FileFilter(name=_("{ext} files").format(ext=extension)) 40 | current_extension_filter.add_suffix(extension) 41 | 42 | audio_files_filter = Gtk.FileFilter(name=_("Audio files")) 43 | audio_files_filter.add_mime_type("audio/*") 44 | 45 | filters = Gio.ListStore.new(Gtk.FileFilter) 46 | filters.append(current_extension_filter) 47 | filters.append(audio_files_filter) 48 | 49 | file_dialog.set_filters(filters) 50 | file_dialog.set_default_filter(current_extension_filter) 51 | file_dialog.open(self.main_window.window, None, self._file_dialog_open_callback) 52 | 53 | def _file_dialog_open_callback(self, dialog, result): 54 | try: 55 | file = dialog.open_finish(result) 56 | except GLib.GError: 57 | pass 58 | else: 59 | if file is not None: 60 | self.missing_chapter.file = file.get_path() 61 | self._importer.scan() 62 | 63 | def present(self) -> None: 64 | super().present(self.main_window.window) 65 | 66 | -------------------------------------------------------------------------------- /cozy/ui/widgets/welcome_dialog.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from pathlib import Path 3 | 4 | import inject 5 | from gi.repository import Adw, Gtk 6 | 7 | from cozy.settings import ApplicationSettings 8 | from cozy.ui.widgets.storages import ask_storage_location 9 | from cozy.view_model.storages_view_model import StoragesViewModel 10 | 11 | 12 | @Gtk.Template.from_resource("/com/github/geigi/cozy/ui/welcome_dialog.ui") 13 | class WelcomeDialog(Adw.Dialog): 14 | __gtype_name__ = "WelcomeDialog" 15 | 16 | app_settings: ApplicationSettings = inject.attr(ApplicationSettings) 17 | _storages_view_model: StoragesViewModel = inject.attr(StoragesViewModel) 18 | 19 | carousel: Adw.Carousel = Gtk.Template.Child() 20 | welcome_page: Adw.StatusPage = Gtk.Template.Child() 21 | reporting_page: Gtk.Box = Gtk.Template.Child() 22 | locations_page: Adw.StatusPage = Gtk.Template.Child() 23 | create_directory_switch: Adw.SwitchRow = Gtk.Template.Child() 24 | chooser_button_label: Adw.ButtonContent = Gtk.Template.Child() 25 | 26 | def __init__(self): 27 | super().__init__() 28 | self._path = None 29 | self._order = [self.welcome_page, self.reporting_page, self.locations_page] 30 | 31 | @Gtk.Template.Callback() 32 | def advance(self, *_): 33 | self.carousel.scroll_to(self._order[int(self.carousel.get_position()) + 1], True) 34 | 35 | @Gtk.Template.Callback() 36 | def deny_reporting(self, _): 37 | self.app_settings.report_level = 0 38 | self.advance() 39 | 40 | @Gtk.Template.Callback() 41 | def accept_reporting(self, _): 42 | self.advance() 43 | 44 | @Gtk.Template.Callback() 45 | def choose_directory(self, _): 46 | ask_storage_location(self._ask_storage_location_callback, None) 47 | 48 | def _ask_storage_location_callback(self, path): 49 | self._path = path 50 | self.chooser_button_label.set_label(os.path.basename(path)) 51 | 52 | @Gtk.Template.Callback() 53 | def done(self, __chooser_button_label): 54 | self.close() 55 | self.app_settings.first_launch = False 56 | 57 | if self.create_directory_switch.props.active: 58 | audiobooks_dir = Path.home() / _("Audiobooks") 59 | audiobooks_dir.mkdir(exist_ok=True) 60 | self._storages_view_model.add_storage_location(str(audiobooks_dir), default=True) 61 | 62 | inject.instance("MainWindow")._set_audiobook_path(self._path, default=False) 63 | else: 64 | inject.instance("MainWindow")._set_audiobook_path(self._path) 65 | -------------------------------------------------------------------------------- /cozy/ui/app_view.py: -------------------------------------------------------------------------------- 1 | import inject 2 | from gi.repository import Adw, Gtk 3 | 4 | from cozy.enums import View 5 | from cozy.view_model.app_view_model import AppViewModel 6 | 7 | LIBRARY = "main" 8 | EMPTY_STATE = "welcome" 9 | PREPARING_LIBRARY = "import" 10 | BOOK_DETAIL = "book_overview" 11 | 12 | 13 | class AppView: 14 | _view_model: AppViewModel = inject.attr(AppViewModel) 15 | 16 | def __init__(self, builder: Gtk.Builder): 17 | self._builder = builder 18 | 19 | self._get_ui_elements() 20 | self._connect_view_model() 21 | self._connect_ui_elements() 22 | 23 | self._update_view_model_view(None, None) 24 | 25 | def _get_ui_elements(self): 26 | self._main_stack: Gtk.Stack = self._builder.get_object("main_stack") 27 | self._navigation_view: Adw.NavigationView = self._builder.get_object("navigation_view") 28 | 29 | def _connect_ui_elements(self): 30 | self._main_stack.connect("notify::visible-child", self._update_view_model_view) 31 | self._navigation_view.connect("notify::visible-page", self._update_view_model_view) 32 | 33 | def _connect_view_model(self): 34 | self._view_model.bind_to("view", self._on_view_changed) 35 | self._view_model.bind_to("open_book_overview", self._on_open_book_overview) 36 | 37 | def _on_open_book_overview(self): 38 | if self._navigation_view.props.visible_page.props.tag == "book_overview": 39 | self._navigation_view.pop_to_tag("book_overview") 40 | else: 41 | self._navigation_view.push_by_tag("book_overview") 42 | 43 | def _on_view_changed(self): 44 | view = self._view_model.view 45 | 46 | if view == View.EMPTY_STATE: 47 | self._main_stack.set_visible_child_name(EMPTY_STATE) 48 | elif view == View.PREPARING_LIBRARY: 49 | self._main_stack.set_visible_child_name(PREPARING_LIBRARY) 50 | elif view == View.LIBRARY: 51 | self._main_stack.set_visible_child_name(LIBRARY) 52 | 53 | def _update_view_model_view(self, *_): 54 | page = self._main_stack.props.visible_child_name 55 | 56 | if page == LIBRARY: 57 | if self._navigation_view.props.visible_page.props.tag == BOOK_DETAIL: 58 | self._view_model.view = View.BOOK_DETAIL 59 | else: 60 | self._view_model.view = View.LIBRARY 61 | elif page == EMPTY_STATE: 62 | self._view_model.view = View.EMPTY_STATE 63 | elif page == PREPARING_LIBRARY: 64 | self._view_model.view = View.PREPARING_LIBRARY 65 | 66 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.playback-speed-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.network-folder-symbolic.svg: -------------------------------------------------------------------------------- 1 | 5 | 13 | 21 | 29 | 37 | 45 | 55 | -------------------------------------------------------------------------------- /po/extra/extra.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: extra\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: data/com.github.geigi.cozy.desktop:3 21 | msgid "Cozy" 22 | msgstr "" 23 | 24 | #: data/com.github.geigi.cozy.desktop:4 25 | msgid "Audio Book Player" 26 | msgstr "" 27 | 28 | #: data/com.github.geigi.cozy.desktop:5 29 | msgid "Play and organize your audio book collection" 30 | msgstr "" 31 | 32 | #: data/com.github.geigi.cozy.appdata.xml:15 33 | msgid "Listen to audio books" 34 | msgstr "" 35 | 36 | #: data/com.github.geigi.cozy.appdata.xml:17 37 | msgid "Do you like audio books? Then lets get cozy!" 38 | msgstr "" 39 | 40 | #: data/com.github.geigi.cozy.appdata.xml:18 41 | msgid "Cozy is a audio book player. Here are some of the features:" 42 | msgstr "" 43 | 44 | #: data/com.github.geigi.cozy.appdata.xml:20 45 | msgid "Import all your audio books into Cozy to browse them comfortably" 46 | msgstr "" 47 | 48 | #: data/com.github.geigi.cozy.appdata.xml:21 49 | msgid "" 50 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 51 | "audio books" 52 | msgstr "" 53 | 54 | #: data/com.github.geigi.cozy.appdata.xml:22 55 | msgid "Remembers your playback position" 56 | msgstr "" 57 | 58 | #: data/com.github.geigi.cozy.appdata.xml:23 59 | msgid "Sleep timer" 60 | msgstr "" 61 | 62 | #: data/com.github.geigi.cozy.appdata.xml:24 63 | msgid "Playback speed control for each book individually" 64 | msgstr "" 65 | 66 | #: data/com.github.geigi.cozy.appdata.xml:25 67 | msgid "Search your library" 68 | msgstr "" 69 | 70 | #: data/com.github.geigi.cozy.appdata.xml:26 71 | msgid "Multiple storage location support" 72 | msgstr "" 73 | 74 | #: data/com.github.geigi.cozy.appdata.xml:27 75 | msgid "" 76 | "Offline Mode! This allows you to keep an audio book on your internal storage " 77 | "if you store your audio books on an external or network drive. Perfect to " 78 | "listen to on the go!" 79 | msgstr "" 80 | 81 | #: data/com.github.geigi.cozy.appdata.xml:28 82 | msgid "Drag and Drop to import new audio books" 83 | msgstr "" 84 | 85 | #: data/com.github.geigi.cozy.appdata.xml:29 86 | msgid "Sort your audio books by author, reader and name" 87 | msgstr "" 88 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.library-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /cozy/view_model/search_view_model.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import inject 4 | from gi.repository import GLib 5 | 6 | from cozy.architecture.event_sender import EventSender 7 | from cozy.architecture.observable import Observable 8 | from cozy.control.filesystem_monitor import FilesystemMonitor 9 | from cozy.enums import OpenView 10 | from cozy.model.book import Book 11 | from cozy.model.library import Library, split_strings_to_set 12 | from cozy.settings import ApplicationSettings 13 | 14 | 15 | class SearchViewModel(Observable, EventSender): 16 | _fs_monitor: FilesystemMonitor = inject.attr("FilesystemMonitor") 17 | _model: Library = inject.attr(Library) 18 | _application_settings: ApplicationSettings = inject.attr(ApplicationSettings) 19 | 20 | def __init__(self): 21 | super().__init__() 22 | super(Observable, self).__init__() 23 | 24 | def _get_available_books(self) -> list[Book]: 25 | is_book_online = self._fs_monitor.get_book_online 26 | 27 | if self._application_settings.hide_offline: 28 | return [book for book in self._model.books if is_book_online(book)] 29 | else: 30 | return self._model.books 31 | 32 | def search( 33 | self, search_query: str, callback: Callable[[list[Book], list[str], list[str]], None] 34 | ) -> None: 35 | search_query = search_query.lower() 36 | 37 | available_books = self._get_available_books() 38 | books = { 39 | book 40 | for book in available_books 41 | if search_query in book.name.lower() 42 | or search_query in book.author.lower() 43 | or search_query in book.reader.lower() 44 | } 45 | 46 | available_book_authors = split_strings_to_set({book.author for book in available_books}) 47 | authors = {author for author in available_book_authors if search_query in author.lower()} 48 | 49 | available_book_readers = split_strings_to_set({book.reader for book in available_books}) 50 | readers = {reader for reader in available_book_readers if search_query in reader.lower()} 51 | 52 | GLib.MainContext.default().invoke_full( 53 | GLib.PRIORITY_DEFAULT, 54 | callback, 55 | sorted(books, key=lambda book: book.name.lower()), 56 | sorted(authors), 57 | sorted(readers), 58 | ) 59 | 60 | def close(self) -> None: 61 | self._notify("close") 62 | 63 | def jump_to_book(self, book: Book) -> None: 64 | self.emit_event(OpenView.BOOK, book) 65 | self.close() 66 | 67 | def jump_to_author(self, author: str) -> None: 68 | self.emit_event(OpenView.AUTHOR, author) 69 | self.close() 70 | 71 | def jump_to_reader(self, reader: str) -> None: 72 | self.emit_event(OpenView.READER, reader) 73 | self.close() 74 | -------------------------------------------------------------------------------- /data/ui/welcome_dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $WelcomeDialog: Adw.Dialog { 5 | width-request: 360; 6 | content-width: 450; 7 | content-height: 600; 8 | 9 | Adw.Carousel carousel { 10 | interactive: false; 11 | 12 | Adw.StatusPage welcome_page { 13 | icon-name: 'com.github.geigi.cozy'; 14 | title: "Let's Get Cozy!"; 15 | hexpand: true; 16 | 17 | Button { 18 | halign: center; 19 | label: _("Start!"); 20 | 21 | styles ["pill", "suggested-action"] 22 | 23 | clicked => $advance(); 24 | } 25 | } 26 | 27 | Box reporting_page{ 28 | orientation: vertical; 29 | spacing: 12; 30 | 31 | Adw.StatusPage { 32 | valign: start; 33 | margin-start: 12; 34 | margin-end: 12; 35 | title: "Error Reporting"; 36 | 37 | styles ["compact"] 38 | 39 | child: $ErrorReporting {}; 40 | } 41 | 42 | CenterBox { 43 | valign: end; 44 | vexpand: true; 45 | 46 | [center] 47 | Box { 48 | spacing: 12; 49 | margin-bottom: 12; 50 | homogeneous: true; 51 | 52 | Button { 53 | label: "I Don't Want This"; 54 | clicked => $deny_reporting(); 55 | } 56 | Button { 57 | label: "It's Fine by Me"; 58 | clicked => $accept_reporting(); 59 | } 60 | } 61 | } 62 | } 63 | Adw.StatusPage locations_page { 64 | title: _("Add Audiobooks"); 65 | margin-start: 12; 66 | margin-end: 12; 67 | 68 | child: Box { 69 | orientation: vertical; 70 | spacing: 18; 71 | 72 | Adw.PreferencesGroup { 73 | Adw.SwitchRow create_directory_switch { 74 | title: _("Create Default Audiobooks Directory"); 75 | subtitle: _("This will create a dedicated directory for audiobooks in your home directory"); 76 | active: true; 77 | } 78 | 79 | Adw.ActionRow { 80 | selectable: false; 81 | activatable: true; 82 | title: _("Audiobooks Directory"); 83 | subtitle: _("You can add more locations in the settings"); 84 | 85 | activated => $choose_directory(); 86 | 87 | [suffix] 88 | Button { 89 | valign: center; 90 | clicked => $choose_directory(); 91 | 92 | Adw.ButtonContent chooser_button_label { 93 | icon-name: "cozy.folder-symbolic"; 94 | can-shrink: true; 95 | } 96 | } 97 | } 98 | } 99 | 100 | Button { 101 | halign: center; 102 | label: _("Done"); 103 | 104 | styles ["pill", "suggested-action"] 105 | 106 | clicked => $done(); 107 | } 108 | }; 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/actions/cozy.feedback-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/cozy/model/test_storage.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_path_returns_correct_value(peewee_database_storage): 5 | from cozy.model.storage import Storage 6 | 7 | storage = Storage(peewee_database_storage, 1) 8 | assert storage.path == "/media/test" 9 | 10 | 11 | def test_setting_path_updates_in_track_object_and_database(peewee_database_storage): 12 | from cozy.db.storage import Storage as StorageModel 13 | from cozy.model.storage import Storage 14 | 15 | new_path = "/tmp/media2" 16 | 17 | storage = Storage(peewee_database_storage, 1) 18 | storage.path = new_path 19 | assert storage.path == new_path 20 | assert StorageModel.get_by_id(1).path == new_path 21 | 22 | 23 | def test_setting_invalid_path_raises_exception(peewee_database_storage): 24 | from cozy.model.storage import InvalidPath, Storage 25 | 26 | invalid_path = "not an absolute path" 27 | storage = Storage(peewee_database_storage, 1) 28 | 29 | with pytest.raises(InvalidPath): 30 | storage.path = invalid_path 31 | 32 | 33 | def test_location_type_returns_correct_default_value(peewee_database_storage): 34 | from cozy.model.storage import Storage 35 | 36 | storage = Storage(peewee_database_storage, 1) 37 | assert storage.location_type == 0 38 | 39 | 40 | def test_setting_location_type_updates_in_track_object_and_database(peewee_database_storage): 41 | from cozy.db.storage import Storage as StorageModel 42 | from cozy.model.storage import Storage 43 | 44 | new_location_type = 555 45 | 46 | storage = Storage(peewee_database_storage, 1) 47 | storage.location_type = new_location_type 48 | assert storage.location_type == new_location_type 49 | assert StorageModel.get_by_id(1).location_type == new_location_type 50 | 51 | 52 | def test_default_returns_correct_default_value(peewee_database_storage): 53 | from cozy.model.storage import Storage 54 | 55 | storage = Storage(peewee_database_storage, 1) 56 | assert not storage.default 57 | 58 | 59 | def test_setting_default_updates_in_track_object_and_database(peewee_database_storage): 60 | from cozy.db.storage import Storage as StorageModel 61 | from cozy.model.storage import Storage 62 | 63 | new_default = True 64 | 65 | storage = Storage(peewee_database_storage, 1) 66 | storage.default = new_default 67 | assert storage.default == new_default 68 | assert StorageModel.get_by_id(1).default == new_default 69 | 70 | 71 | def test_external_returns_correct_default_value(peewee_database_storage): 72 | from cozy.model.storage import Storage 73 | 74 | storage = Storage(peewee_database_storage, 1) 75 | assert not storage.external 76 | 77 | 78 | def test_setting_external_updates_in_track_object_and_database(peewee_database_storage): 79 | from cozy.db.storage import Storage as StorageModel 80 | from cozy.model.storage import Storage 81 | 82 | new_external = True 83 | 84 | storage = Storage(peewee_database_storage, 1) 85 | storage.external = new_external 86 | assert storage.external == new_external 87 | assert StorageModel.get_by_id(1).external == new_external 88 | -------------------------------------------------------------------------------- /cozy/report/report_to_loki.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import platform 4 | from contextlib import suppress 5 | 6 | import distro 7 | import inject 8 | import pytz 9 | import requests 10 | from gi.repository import Gtk 11 | from mutagen import version_string as MutagenVersion 12 | from peewee import __version__ as PeeweeVersion 13 | 14 | from cozy import __version__ as CozyVersion 15 | from cozy.report.log_level import LogLevel 16 | from cozy.settings import ApplicationSettings 17 | 18 | URL = 'https://errors.cozy.sh:3100/api/prom/push' 19 | ENABLE = '@INSTALLED@' 20 | 21 | LOG_LEVEL_MAP = { 22 | LogLevel.DEBUG: "DEBUG", 23 | LogLevel.INFO: "INFO", 24 | LogLevel.WARNING: "WARN", 25 | LogLevel.ERROR: "ERROR" 26 | } 27 | 28 | 29 | def report(component: str, type: LogLevel, message: str, exception: Exception): 30 | if ENABLE != 'true': 31 | return 32 | 33 | app_settings = inject.instance(ApplicationSettings) 34 | report_level = app_settings.report_level 35 | 36 | if report_level == 0: 37 | return 38 | 39 | curr_datetime = datetime.datetime.now(pytz.timezone('Europe/Berlin')) 40 | curr_datetime = curr_datetime.isoformat('T') 41 | 42 | if not component or not type or not message: 43 | raise ValueError("component, type and message are mandatory") 44 | 45 | labels = __append_label("", "component", component) 46 | 47 | if exception: 48 | labels = __append_label(labels, "exception_type", exception.__class__.__name__) 49 | 50 | labels = __append_label(labels, "app", "cozy") 51 | labels = __append_label(labels, "level", LOG_LEVEL_MAP[type]) 52 | 53 | labels = __append_label(labels, "gtk_version", f"{Gtk.get_major_version()}.{Gtk.get_minor_version()}") 54 | labels = __append_label(labels, "python_version", platform.python_version()) 55 | labels = __append_label(labels, "peewee_version", PeeweeVersion) 56 | labels = __append_label(labels, "mutagen_version", MutagenVersion) 57 | labels = __append_label(labels, "version", CozyVersion) 58 | 59 | if report_level > 1: 60 | labels = __append_label(labels, "distro", distro.name()) 61 | labels = __append_label(labels, "distro_version", distro.version()) 62 | labels = __append_label(labels, "desktop_environment", os.environ.get('DESKTOP_SESSION')) 63 | 64 | line = f"[{LOG_LEVEL_MAP[type]}] {message}" 65 | 66 | headers = { 67 | 'Content-type': 'application/json' 68 | } 69 | payload = { 70 | 'streams': [ 71 | { 72 | 'labels': f"{{{labels}}}", 73 | 'entries': [ 74 | { 75 | 'ts': curr_datetime, 76 | 'line': line 77 | } 78 | ] 79 | } 80 | ] 81 | } 82 | 83 | with suppress(Exception): 84 | requests.post(URL, json=payload, headers=headers, timeout=10) 85 | 86 | 87 | def __append_label(labels, new_label_name, new_label_content): 88 | if labels: 89 | labels += "," 90 | else: 91 | labels = "" 92 | 93 | labels += f"{new_label_name}=\"{new_label_content}\"" 94 | 95 | return labels 96 | -------------------------------------------------------------------------------- /data/ui/sleep_timer_dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $SleepTimer: Adw.Dialog { 5 | width-request: 250; 6 | content-width: 380; 7 | 8 | title: _("Sleep Timer"); 9 | 10 | child: Adw.ToolbarView toolbarview { 11 | [top] 12 | Adw.HeaderBar headerbar {} 13 | 14 | [bottom] 15 | ActionBar button_bar { 16 | [center] 17 | Box { 18 | spacing: 12; 19 | margin-bottom: 12; 20 | homogeneous: true; 21 | Button cancel_button { 22 | label: _("Cancel"); 23 | clicked => $close(); 24 | } 25 | 26 | Button set_timer_button { 27 | label: _("Set Timer"); 28 | sensitive: false; 29 | clicked => $set_timer(); 30 | styles ["suggested-action"] 31 | } 32 | } 33 | } 34 | 35 | content: Stack stack { 36 | margin-start: 18; 37 | margin-end: 18; 38 | margin-top: 12; 39 | margin-bottom: 12; 40 | 41 | StackPage { 42 | name: "uninitiated"; 43 | child: Box { 44 | orientation: vertical; 45 | spacing: 12; 46 | 47 | Adw.PreferencesGroup list {} 48 | Adw.PreferencesGroup { 49 | Adw.ComboRow power_action_combo_row { 50 | title: _("System Power Control"); 51 | subtitle: _("Action to perform when timer finishes"); 52 | notify::selected => $on_power_action_selected(); 53 | 54 | model: StringList { 55 | strings [ 56 | _("None"), 57 | _("Suspend"), 58 | _("Shutdown") 59 | ] 60 | }; 61 | } 62 | } 63 | }; 64 | } 65 | 66 | visible-child-name: "uninitiated"; 67 | 68 | StackPage { 69 | name: "running"; 70 | child: Adw.StatusPage timer_state { 71 | styles ["numeric"] 72 | child: Adw.PreferencesGroup { 73 | separate-rows: true; 74 | 75 | Adw.ButtonRow { 76 | title: _("+ 5 minutes"); 77 | activated => $plus_5_minutes(); 78 | } 79 | Adw.ButtonRow till_end_of_chapter_button_row { 80 | title: _("End of Chapter"); 81 | activated => $till_end_of_chapter(); 82 | } 83 | Adw.ButtonRow { 84 | title: _("Cancel Timer"); 85 | activated => $cancel_timer(); 86 | styles ["suggested-action"] 87 | } 88 | Adw.ComboRow { 89 | title: _("System Power Control"); 90 | subtitle: _("Action to perform when timer finishes"); 91 | notify::selected => $on_power_action_selected(); 92 | selected: bind power_action_combo_row.selected bidirectional; 93 | 94 | model: StringList { 95 | strings [ 96 | _("None"), 97 | _("Suspend"), 98 | _("Shutdown") 99 | ] 100 | }; 101 | } 102 | }; 103 | }; 104 | } 105 | }; 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /cozy/model/settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import NoReturn 3 | 4 | import inject 5 | import peewee 6 | from peewee import SqliteDatabase 7 | 8 | from cozy.db.book import Book 9 | from cozy.db.settings import Settings as SettingsModel 10 | from cozy.db.storage import Storage as StorageModel 11 | from cozy.model.storage import InvalidPath, Storage 12 | from cozy.report import reporter 13 | 14 | log = logging.getLogger("model.storage_location") 15 | 16 | 17 | class Settings: 18 | _storages: list[Storage] = [] 19 | _db = inject.attr(SqliteDatabase) 20 | 21 | def __init__(self): 22 | self._db_object: SettingsModel = SettingsModel.get() 23 | 24 | @property 25 | def first_start(self) -> bool: 26 | return self._db_object.first_start 27 | 28 | @property 29 | def last_played_book(self) -> Book | None: 30 | try: 31 | return self._db_object.last_played_book 32 | except peewee.DoesNotExist: 33 | log.warning( 34 | "last_played_book references an non existent object. Setting last_played_book to None." 35 | ) 36 | reporter.warning( 37 | "settings_model", 38 | "last_played_book references an non existent object. Setting last_played_book to None.", 39 | ) 40 | 41 | self.last_played_book = None 42 | return None 43 | 44 | @last_played_book.setter 45 | def last_played_book(self, new_value) -> None: 46 | if new_value: 47 | self._db_object.last_played_book = new_value._db_object 48 | else: 49 | self._db_object.last_played_book = None 50 | 51 | self._db_object.save(only=self._db_object.dirty_fields) 52 | 53 | @property 54 | def default_location(self) -> Storage | NoReturn: 55 | for location in self.storage_locations: 56 | if location.default: 57 | return location 58 | raise AssertionError("This should never happen") 59 | 60 | @property 61 | def storage_locations(self) -> list[Storage]: 62 | if not self._storages: 63 | self._load_all_storage_locations() 64 | 65 | return self._storages 66 | 67 | @property 68 | def external_storage_locations(self) -> list[Storage]: 69 | if not self._storages: 70 | self._load_all_storage_locations() 71 | 72 | return [storage for storage in self._storages if storage.external] 73 | 74 | def invalidate(self) -> None: 75 | self._storages.clear() 76 | 77 | def _load_all_storage_locations(self) -> None: 78 | self.invalidate() 79 | 80 | for storage_db_obj in StorageModel.select(StorageModel.id): 81 | try: 82 | self._storages.append(Storage(self._db, storage_db_obj.id)) 83 | except InvalidPath: 84 | log.error("Invalid path found in database, skipping: %s", storage_db_obj.path) 85 | 86 | self._ensure_default_storage_is_present() 87 | 88 | def _ensure_default_storage_is_present(self): 89 | default_storage_present = any(storage.default for storage in self._storages) 90 | 91 | if not default_storage_present and self._storages: 92 | self._storages[0].default = True 93 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('com.github.geigi.cozy', version: '1.3.0', meson_version: '>= 0.59.0') 2 | 3 | python = import('python') 4 | i18n = import('i18n') 5 | gnome = import('gnome') 6 | 7 | message('Looking for dependencies') 8 | python_bin = python.find_installation('python3') 9 | if not python_bin.found() 10 | error('No valid python3 binary found') 11 | else 12 | message('Found python3 binary') 13 | endif 14 | 15 | dependency('glib-2.0') 16 | dependency('libadwaita-1', version: '>= 1.5.0') 17 | 18 | # from https://github.com/AsavarTzeth/pulseeffects/blob/master/meson.build 19 | # Support Debian non-standard python paths 20 | # Fallback to Meson python module if command fails 21 | message('Getting python install path') 22 | py3_purelib = '' 23 | r = run_command( 24 | python_bin.full_path(), 25 | '-c', 26 | 'from distutils.sysconfig import get_python_lib; print(get_python_lib(prefix=""))', 27 | check: false, 28 | ) 29 | 30 | if r.returncode() != 0 31 | py3_purelib = python_bin.get_path('purelib') 32 | if not py3_purelib.endswith('site-packages') 33 | error('Cannot find python install path') 34 | endif 35 | python_dir = py3_purelib 36 | else 37 | python_dir = r.stdout().strip() 38 | endif 39 | 40 | # Python 3 required modules 41 | python3_required_modules = ['distro', 'mutagen', 'peewee', 'pytz', 'requests', 'inject', 'gi'] 42 | 43 | foreach p : python3_required_modules 44 | # Source: https://docs.python.org/3/library/importlib.html#checking-if-a-module-can-be-imported 45 | script = 'import importlib.util; import sys; exit(1) if importlib.util.find_spec(\''+ p +'\') is None else exit(0)' 46 | if run_command(python_bin, '-c', script, check: false).returncode() != 0 47 | error('Required Python3 module \'' + p + '\' not found') 48 | endif 49 | endforeach 50 | 51 | LIBEXEC_DIR = join_paths(get_option('prefix'), 'libexec') 52 | DATA_DIR = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 53 | 54 | conf = configuration_data() 55 | conf.set('PACKAGE_URL', 'https://github.com/geigi/cozy') 56 | conf.set('DATA_DIR', DATA_DIR) 57 | conf.set('LOCALE_DIR', join_paths(get_option('prefix'), get_option('datadir'), 'locale')) 58 | conf.set('PYTHON_DIR', python_dir) 59 | conf.set('PYTHON_EXEC_DIR', join_paths(get_option('prefix'), python_bin.get_path('stdlib'))) 60 | conf.set('libexecdir', LIBEXEC_DIR) 61 | conf.set('VERSION', meson.project_version()) 62 | conf.set('PYTHON', python_bin.full_path()) 63 | conf.set('INSTALLED', 'true') 64 | 65 | subdir('data') 66 | subdir('po') 67 | 68 | message('Compile gnome schemas') 69 | 70 | message('Preparing init file') 71 | configure_file( 72 | input: 'main.py', 73 | output: 'com.github.geigi.cozy', 74 | configuration: conf, 75 | install_dir: 'bin' 76 | ) 77 | 78 | install_subdir( 79 | 'cozy', 80 | install_dir: python_dir 81 | ) 82 | 83 | configure_file( 84 | input: 'cozy/__init__.py', 85 | output: '__init__.py', 86 | configuration: conf, 87 | install_dir: python_dir + '/cozy' 88 | ) 89 | 90 | configure_file( 91 | input: 'cozy/report/report_to_loki.py', 92 | output: 'report_to_loki.py', 93 | configuration: conf, 94 | install_dir: python_dir + '/cozy/report' 95 | ) 96 | 97 | gnome.post_install( 98 | glib_compile_schemas: true, 99 | gtk_update_icon_cache: true, 100 | update_desktop_database: true, 101 | ) 102 | -------------------------------------------------------------------------------- /cozy/control/time_format.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from gettext import ngettext 3 | 4 | from gi.repository import Gst 5 | 6 | 7 | def seconds_to_time(seconds: int) -> str: 8 | return ns_to_time(seconds * Gst.SECOND) 9 | 10 | 11 | def ns_to_time( 12 | nanoseconds: int, max_length: int | None = None, include_seconds: bool = True 13 | ) -> str: 14 | """ 15 | Converts nanoseconds to a string with the following appearance: 16 | hh:mm:ss 17 | 18 | :param nanoseconds: int 19 | """ 20 | m, s = divmod(nanoseconds / Gst.SECOND, 60) 21 | h, m = divmod(m, 60) 22 | 23 | if max_length: 24 | max_m, _ = divmod(max_length, 60) 25 | max_h, max_m = divmod(max_m, 60) 26 | else: 27 | max_h = h 28 | max_m = m 29 | 30 | if max_h >= 10: 31 | result = "%02d:%02d" % (h, m) 32 | elif max_h >= 1: 33 | result = "%d:%02d" % (h, m) 34 | else: 35 | result = "%02d" % m 36 | 37 | if include_seconds: 38 | result += ":%02d" % s 39 | 40 | return result 41 | 42 | 43 | def min_to_human_readable(minutes: int) -> str: 44 | return ns_to_human_readable(minutes * Gst.SECOND * 60) 45 | 46 | 47 | def ns_to_human_readable(nanoseconds: int) -> str: 48 | """ 49 | Create a string with the following format: 50 | 6 hours 1 minute 51 | 45 minutes 52 | 21 seconds 53 | :param seconds: int 54 | """ 55 | m, s = divmod(nanoseconds / Gst.SECOND, 60) 56 | h, m = divmod(m, 60) 57 | h = int(h) 58 | m = int(m) 59 | s = int(s) 60 | 61 | result = "" 62 | if h > 0 and m > 0: 63 | result = ( 64 | ngettext("{hours} hour", "{hours} hours", h).format(hours=h) 65 | + " " 66 | + ngettext("{minutes} minute", "{minutes} minutes", m).format(minutes=m) 67 | ) 68 | elif h > 0: 69 | result = ngettext("{hours} hour", "{hours} hours", h).format(hours=h) 70 | elif m > 0: 71 | result = ngettext("{minutes} minute", "{minutes} minutes", m).format(minutes=m) 72 | elif s > 0: 73 | result = ngettext("{seconds} second", "{seconds} seconds", s).format(seconds=s) 74 | else: 75 | result = _("finished") 76 | 77 | return result 78 | 79 | 80 | def date_delta_to_human_readable(unix_time): 81 | """ 82 | Converts the date to the following strings (from today): 83 | today 84 | yesterday 85 | x days ago 86 | x week(s) ago 87 | x month(s) ago 88 | x year(s) ago 89 | """ 90 | date = datetime.fromtimestamp(unix_time) 91 | past = datetime.today().date() - date.date() 92 | days = int(past.days) 93 | weeks = int(days / 7) 94 | months = int(days / 30) 95 | years = int(months / 12) 96 | 97 | if unix_time < 1: 98 | return _("never") 99 | elif days < 1: 100 | return _("today") 101 | elif days < 2: 102 | return _("yesterday") 103 | elif days < 7: 104 | return _("{days} days ago").format(days=days) 105 | elif weeks < 5: 106 | return ngettext("{weeks} week ago", "{weeks} weeks ago", weeks).format(weeks=weeks) 107 | elif months < 12: 108 | return ngettext("{months} month ago", "{months} months ago", months).format(months=months) 109 | else: 110 | return ngettext("{years} year ago", "{years} years ago", years).format(years=years) 111 | -------------------------------------------------------------------------------- /data/ui/book_card.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $BookCard: FlowBoxChild { 5 | focusable: false; 6 | 7 | Adw.Clamp { 8 | maximum-size: 250; 9 | Overlay { 10 | [overlay] 11 | Revealer play_revealer { 12 | transition-type: crossfade; 13 | valign: end; 14 | halign: end; 15 | 16 | $BookCardPlayButton play_button { 17 | width-request: 54; 18 | height-request: 54; 19 | margin-end: 18; 20 | margin-bottom: 18; 21 | focusable: true; 22 | focus-on-click: false; 23 | icon-name: "media-playback-start-symbolic"; 24 | tooltip-text: _("Start/Stop playback"); 25 | 26 | clicked => $_play_pause(); 27 | 28 | accessibility { 29 | label: _("Start or pause the playback"); 30 | } 31 | 32 | styles [ 33 | "circular", 34 | "suggested-action", 35 | ] 36 | } 37 | } 38 | 39 | [overlay] 40 | Revealer menu_revealer { 41 | transition-type: crossfade; 42 | valign: start; 43 | halign: end; 44 | 45 | MenuButton menu_button { 46 | margin-end: 6; 47 | margin-top: 6; 48 | menu-model: book_menu; 49 | icon-name: "view-more-symbolic"; 50 | tooltip-text: _("Open Book Menu"); 51 | 52 | styles [ 53 | "circular", 54 | "opaque", 55 | ] 56 | } 57 | } 58 | 59 | Button button { 60 | overflow: hidden; 61 | tooltip-text: _("Open Book Overview"); 62 | 63 | clicked => $_open_book_overview(); 64 | 65 | accessibility { 66 | labelled-by: title; 67 | } 68 | 69 | Box { 70 | orientation: vertical; 71 | 72 | Stack stack { 73 | Picture artwork { 74 | content-fit: cover; 75 | hexpand: true; 76 | vexpand: true; 77 | } 78 | 79 | Image fallback_icon { 80 | pixel-size: 200; 81 | hexpand: true; 82 | vexpand: true; 83 | } 84 | } 85 | 86 | Label title { 87 | label: bind template.title; 88 | tooltip-text: bind template.title; 89 | ellipsize: end; 90 | hexpand: false; 91 | halign: start; 92 | margin-top: 12; 93 | margin-start: 12; 94 | margin-end: 12; 95 | xalign: 0; 96 | 97 | styles [ 98 | "heading", 99 | ] 100 | } 101 | 102 | Label { 103 | label: bind template.author; 104 | tooltip-text: bind template.author; 105 | ellipsize: end; 106 | hexpand: false; 107 | halign: start; 108 | margin-top: 3; 109 | margin-bottom: 12; 110 | margin-start: 12; 111 | margin-end: 12; 112 | xalign: 0; 113 | 114 | styles [ 115 | "dim-label", 116 | "caption", 117 | ] 118 | } 119 | } 120 | styles ["card"] 121 | } 122 | } 123 | } 124 | } 125 | 126 | menu book_menu { 127 | section first_menu_section {} 128 | 129 | section second_menu_section {} 130 | } 131 | -------------------------------------------------------------------------------- /cozy/ui/widgets/seek_bar.py: -------------------------------------------------------------------------------- 1 | from gi.repository import GObject, Gtk 2 | 3 | from cozy.control.time_format import ns_to_time 4 | 5 | 6 | @Gtk.Template.from_resource('/com/github/geigi/cozy/ui/seek_bar.ui') 7 | class SeekBar(Gtk.Box): 8 | __gtype_name__ = "SeekBar" 9 | 10 | progress_scale: Gtk.Scale = Gtk.Template.Child() 11 | current_label: Gtk.Label = Gtk.Template.Child() 12 | remaining_label: Gtk.Label = Gtk.Template.Child() 13 | remaining_event_box: Gtk.Box = Gtk.Template.Child() 14 | 15 | length: float 16 | 17 | def __init__(self, **kwargs): 18 | super().__init__(**kwargs) 19 | 20 | self.length: float = 0.0 21 | self._progress_scale_pressed = False 22 | 23 | self.progress_scale.connect("value-changed", self._on_progress_scale_changed) 24 | 25 | # HACK: Using a GtkGestureClick here is not possible, as GtkRange's internal 26 | # gesture controller claims the button press event, and thus the released signal doesn't get emitted. 27 | # Therefore we get its internal GtkGestureClick, and add our handlers to that. 28 | # Hacky workaround from: https://gitlab.gnome.org/GNOME/gtk/-/issues/4939 29 | # Ideally GtkRange would forward these signals, so we wouldn't need this hack 30 | # TODO: Add these signals to Gtk and make a MR? 31 | for controller in self.progress_scale.observe_controllers(): 32 | if isinstance(controller, Gtk.GestureClick): 33 | click_gesture = controller 34 | break 35 | 36 | click_gesture.set_button(0) # Enable all mouse buttons 37 | click_gesture.connect("pressed", self._on_progress_scale_press) 38 | click_gesture.connect("released", self._on_progress_scale_release) 39 | 40 | @GObject.Signal(arg_types=(object,)) 41 | def position_changed(self, *_): ... 42 | 43 | @GObject.Signal 44 | def rewind(self): ... 45 | 46 | @GObject.Signal 47 | def forward(self): ... 48 | 49 | @property 50 | def position(self) -> float: 51 | return self.progress_scale.get_value() 52 | 53 | @position.setter 54 | def position(self, new_value: float): 55 | if not self._progress_scale_pressed: 56 | self.progress_scale.set_value(new_value) 57 | 58 | @property 59 | def sensitive(self) -> bool: 60 | return self.progress_scale.get_sensitive() 61 | 62 | @sensitive.setter 63 | def sensitive(self, new_value: bool): 64 | self.progress_scale.set_sensitive(new_value) 65 | 66 | @property 67 | def visible(self) -> bool: 68 | return self.progress_scale.get_visible() 69 | 70 | @visible.setter 71 | def visible(self, value: bool): 72 | self.current_label.set_visible(value) 73 | self.progress_scale.set_visible(value) 74 | self.remaining_event_box.set_visible(value) 75 | 76 | def _on_progress_scale_changed(self, _): 77 | total = self.length 78 | position = int(total * self.progress_scale.get_value() / 100) 79 | remaining_secs = int(total - position) 80 | 81 | self.current_label.set_text(ns_to_time(position, total)) 82 | self.remaining_label.set_text(ns_to_time(remaining_secs, total)) 83 | 84 | def _on_progress_scale_release(self, *_): 85 | self._progress_scale_pressed = False 86 | value = self.progress_scale.get_value() 87 | self.emit("position-changed", value) 88 | 89 | def _on_progress_scale_press(self, *_): 90 | self._progress_scale_pressed = True 91 | -------------------------------------------------------------------------------- /po/extra/fa.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Danial Behzadi , 2025 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: extra\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 15 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 16 | "Last-Translator: Danial Behzadi , 2025\n" 17 | "Language-Team: Persian (https://app.transifex.com/geigi/teams/78138/fa/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: fa\n" 22 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 23 | 24 | #: data/com.github.geigi.cozy.desktop:3 25 | msgid "Cozy" 26 | msgstr "دنج" 27 | 28 | #: data/com.github.geigi.cozy.desktop:4 29 | msgid "Audio Book Player" 30 | msgstr "پخش کتاب صوتی" 31 | 32 | #: data/com.github.geigi.cozy.desktop:5 33 | msgid "Play and organize your audio book collection" 34 | msgstr "پخش و سازمان‌دهی مجموعهٔ کتاب‌های صوتیتان" 35 | 36 | #: data/com.github.geigi.cozy.appdata.xml:15 37 | msgid "Listen to audio books" 38 | msgstr "گوش دادن به کتاب‌های صوتی" 39 | 40 | #: data/com.github.geigi.cozy.appdata.xml:17 41 | msgid "Do you like audio books? Then lets get cozy!" 42 | msgstr "کتاب صوتی دوست دارید؟ پس بیایید دنج کنیم!" 43 | 44 | #: data/com.github.geigi.cozy.appdata.xml:18 45 | msgid "Cozy is a audio book player. Here are some of the features:" 46 | msgstr "دنج پخش‌کنندهٔ کتاب صوتی است. برخی ویژگی‌ها:" 47 | 48 | #: data/com.github.geigi.cozy.appdata.xml:20 49 | msgid "Import all your audio books into Cozy to browse them comfortably" 50 | msgstr "درون‌ریزی همهٔ کتاب‌های صوتیتان در دنج برای مرور آسانشان" 51 | 52 | #: data/com.github.geigi.cozy.appdata.xml:21 53 | msgid "" 54 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 55 | "audio books" 56 | msgstr "" 57 | "گوش دادن به کتاب‌های صوتی بدون DRM در قالب‌های‎ ‪ mp3، m4b، m4a (aac، ALAC, " 58 | "…)، flac، ogg و wav" 59 | 60 | #: data/com.github.geigi.cozy.appdata.xml:22 61 | msgid "Remembers your playback position" 62 | msgstr "به خاطر سپردن موقعیت پخشتان" 63 | 64 | #: data/com.github.geigi.cozy.appdata.xml:23 65 | msgid "Sleep timer" 66 | msgstr "زمان‌سنج خواب" 67 | 68 | #: data/com.github.geigi.cozy.appdata.xml:24 69 | msgid "Playback speed control for each book individually" 70 | msgstr "واپایش جداگانهٔ سرعت پخش برای هر کار" 71 | 72 | #: data/com.github.geigi.cozy.appdata.xml:25 73 | msgid "Search your library" 74 | msgstr "جست‌وجوی کتابخانه‌تان" 75 | 76 | #: data/com.github.geigi.cozy.appdata.xml:26 77 | msgid "Multiple storage location support" 78 | msgstr "پشتیبانی از چندین محل ذخیره سازی" 79 | 80 | #: data/com.github.geigi.cozy.appdata.xml:27 81 | msgid "" 82 | "Offline Mode! This allows you to keep an audio book on your internal storage" 83 | " if you store your audio books on an external or network drive. Perfect to " 84 | "listen to on the go!" 85 | msgstr "" 86 | "حالت برون‌خط! می‌گذارد در صورت ذخیرهٔ کردن کتاب‌های صوتیتان روی گردانندهٔ " 87 | "شبکه یا خارحی، آن را روی ذخیره ساز داخلی نگه دارید. عالی برای شنیدن در مسیر!" 88 | 89 | #: data/com.github.geigi.cozy.appdata.xml:28 90 | msgid "Drag and Drop to import new audio books" 91 | msgstr "کشیدن و رها کردن برای درون ریزی کتاب‌های صوتی جدید" 92 | 93 | #: data/com.github.geigi.cozy.appdata.xml:29 94 | msgid "Sort your audio books by author, reader and name" 95 | msgstr "چینش کتاب‌های صوتی بر اساس نگارنده، راوی و نام" 96 | -------------------------------------------------------------------------------- /po/extra/hi.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Panwar108 , 2021 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: extra\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 15 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 16 | "Last-Translator: Panwar108 , 2021\n" 17 | "Language-Team: Hindi (https://app.transifex.com/geigi/teams/78138/hi/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: hi\n" 22 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 23 | 24 | #: data/com.github.geigi.cozy.desktop:3 25 | msgid "Cozy" 26 | msgstr "Cozy" 27 | 28 | #: data/com.github.geigi.cozy.desktop:4 29 | msgid "Audio Book Player" 30 | msgstr "ऑडियो पुस्तक प्लेयर" 31 | 32 | #: data/com.github.geigi.cozy.desktop:5 33 | msgid "Play and organize your audio book collection" 34 | msgstr "ऑडियो पुस्तक संग्रह श्रवण एवं प्रबंधन" 35 | 36 | #: data/com.github.geigi.cozy.appdata.xml:15 37 | msgid "Listen to audio books" 38 | msgstr "ऑडियो पुस्तकें श्रवण करें" 39 | 40 | #: data/com.github.geigi.cozy.appdata.xml:17 41 | msgid "Do you like audio books? Then lets get cozy!" 42 | msgstr "क्या आपकी ऑडियो पुस्तकों में रूचि हैं? यदि हाँ तो Cozy उपयोग करें!" 43 | 44 | #: data/com.github.geigi.cozy.appdata.xml:18 45 | msgid "Cozy is a audio book player. Here are some of the features:" 46 | msgstr "" 47 | "Cozy एक आधुनिक ऑडियो पुस्तक प्लेयर है। इसकी विशेषताएँ निम्नलिखित हैं :" 48 | 49 | #: data/com.github.geigi.cozy.appdata.xml:20 50 | msgid "Import all your audio books into Cozy to browse them comfortably" 51 | msgstr "अपनी ऑडियो पुस्तकों के सरल प्रबंधन हेतु उन सभी को Cozy में आयात करें" 52 | 53 | #: data/com.github.geigi.cozy.appdata.xml:21 54 | msgid "" 55 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 56 | "audio books" 57 | msgstr "" 58 | "डीआरएम मुक्त निःशुल्क mp3, m4b, m4a (aac, ALAC, …), flac, ogg व wav ऑडियो " 59 | "पुस्तकें श्रवण करें" 60 | 61 | #: data/com.github.geigi.cozy.appdata.xml:22 62 | msgid "Remembers your playback position" 63 | msgstr "वाचक स्थिति स्मरण" 64 | 65 | #: data/com.github.geigi.cozy.appdata.xml:23 66 | msgid "Sleep timer" 67 | msgstr "निद्रा टाइमर" 68 | 69 | #: data/com.github.geigi.cozy.appdata.xml:24 70 | msgid "Playback speed control for each book individually" 71 | msgstr "प्रत्येक पुस्तक हेतु वाचक गति नियंत्रण" 72 | 73 | #: data/com.github.geigi.cozy.appdata.xml:25 74 | msgid "Search your library" 75 | msgstr "संग्रह में खोजें" 76 | 77 | #: data/com.github.geigi.cozy.appdata.xml:26 78 | msgid "Multiple storage location support" 79 | msgstr "एकाधिक संचय स्थान समर्थन" 80 | 81 | #: data/com.github.geigi.cozy.appdata.xml:27 82 | msgid "" 83 | "Offline Mode! This allows you to keep an audio book on your internal storage" 84 | " if you store your audio books on an external or network drive. Perfect to " 85 | "listen to on the go!" 86 | msgstr "" 87 | "ऑफलाइन मोड! इस द्वारा आप ऑडियो पुस्तकें आंतरिक स्थान पर संचित कर सकते हैं " 88 | "यदि वे बाह्य या नेटवर्क ड्राइव पर हैं। श्रवण करें कभी भी कहीं भी!" 89 | 90 | #: data/com.github.geigi.cozy.appdata.xml:28 91 | msgid "Drag and Drop to import new audio books" 92 | msgstr "माउस द्वारा ड्रैग कर नवीन ऑडियो पुस्तकें आयात करें" 93 | 94 | #: data/com.github.geigi.cozy.appdata.xml:29 95 | msgid "Sort your audio books by author, reader and name" 96 | msgstr "ऑडियो पुस्तकें लेखक, वाचक व नाम से अनुक्रमित करें" 97 | -------------------------------------------------------------------------------- /po/extra/nl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Heimen Stoffels , 2021 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: extra\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 15 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 16 | "Last-Translator: Heimen Stoffels , 2021\n" 17 | "Language-Team: Dutch (https://app.transifex.com/geigi/teams/78138/nl/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: nl\n" 22 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 23 | 24 | #: data/com.github.geigi.cozy.desktop:3 25 | msgid "Cozy" 26 | msgstr "Cozy" 27 | 28 | #: data/com.github.geigi.cozy.desktop:4 29 | msgid "Audio Book Player" 30 | msgstr "Luisterboekspeler" 31 | 32 | #: data/com.github.geigi.cozy.desktop:5 33 | msgid "Play and organize your audio book collection" 34 | msgstr "Beluister en organiseer je luisterboeken" 35 | 36 | #: data/com.github.geigi.cozy.appdata.xml:15 37 | msgid "Listen to audio books" 38 | msgstr "Speel luisterboeken af" 39 | 40 | #: data/com.github.geigi.cozy.appdata.xml:17 41 | msgid "Do you like audio books? Then lets get cozy!" 42 | msgstr "Speel je graag luisterboeken af? Dan wil je cozy!" 43 | 44 | #: data/com.github.geigi.cozy.appdata.xml:18 45 | msgid "Cozy is a audio book player. Here are some of the features:" 46 | msgstr "Cozy is een luisterboekspeler. Hieronder vind je enkele kenmerken:" 47 | 48 | #: data/com.github.geigi.cozy.appdata.xml:20 49 | msgid "Import all your audio books into Cozy to browse them comfortably" 50 | msgstr "Importeer al je luisterboeken om ze ordelijk te kunnen bekijken" 51 | 52 | #: data/com.github.geigi.cozy.appdata.xml:21 53 | msgid "" 54 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 55 | "audio books" 56 | msgstr "" 57 | "Speel je drm-vrije luisterboeken af (mp3, m4a (aac, alac, …), flac, ogg en " 58 | "wav)" 59 | 60 | #: data/com.github.geigi.cozy.appdata.xml:22 61 | msgid "Remembers your playback position" 62 | msgstr "De afspeelpositie wordt opgeslagen" 63 | 64 | #: data/com.github.geigi.cozy.appdata.xml:23 65 | msgid "Sleep timer" 66 | msgstr "Tijdklok" 67 | 68 | #: data/com.github.geigi.cozy.appdata.xml:24 69 | msgid "Playback speed control for each book individually" 70 | msgstr "Kies bij elk boek de gewenste afspeelsnelheid" 71 | 72 | #: data/com.github.geigi.cozy.appdata.xml:25 73 | msgid "Search your library" 74 | msgstr "Doorzoek je bibliotheek" 75 | 76 | #: data/com.github.geigi.cozy.appdata.xml:26 77 | msgid "Multiple storage location support" 78 | msgstr "Ondersteuning voor meerdere opslaglocaties" 79 | 80 | #: data/com.github.geigi.cozy.appdata.xml:27 81 | msgid "" 82 | "Offline Mode! This allows you to keep an audio book on your internal storage" 83 | " if you store your audio books on an external or network drive. Perfect to " 84 | "listen to on the go!" 85 | msgstr "" 86 | "Offline-modus! Hiermee kun je een luisterboek lokaal opslaan als je ze " 87 | "normaal extern opslaat. Ideaal om onderweg te kunnen luisteren!" 88 | 89 | #: data/com.github.geigi.cozy.appdata.xml:28 90 | msgid "Drag and Drop to import new audio books" 91 | msgstr "Importeer luisterboeken middels slepen-en-neerzetten" 92 | 93 | #: data/com.github.geigi.cozy.appdata.xml:29 94 | msgid "Sort your audio books by author, reader and name" 95 | msgstr "Sorteer je luisterboeken op auteur, voorlezer en naam" 96 | -------------------------------------------------------------------------------- /po/extra/he.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Yaron Shahrabani , 2022 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: extra\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 15 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 16 | "Last-Translator: Yaron Shahrabani , 2022\n" 17 | "Language-Team: Hebrew (https://app.transifex.com/geigi/teams/78138/he/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: he\n" 22 | "Plural-Forms: nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" 23 | 24 | #: data/com.github.geigi.cozy.desktop:3 25 | msgid "Cozy" 26 | msgstr "Cozy" 27 | 28 | #: data/com.github.geigi.cozy.desktop:4 29 | msgid "Audio Book Player" 30 | msgstr "נגן ספרי שמע" 31 | 32 | #: data/com.github.geigi.cozy.desktop:5 33 | msgid "Play and organize your audio book collection" 34 | msgstr "ניתן להשמיע ולסדר את אוסף ספרי השמע שלך" 35 | 36 | #: data/com.github.geigi.cozy.appdata.xml:15 37 | msgid "Listen to audio books" 38 | msgstr "האזנה לספרי שמע" 39 | 40 | #: data/com.github.geigi.cozy.appdata.xml:17 41 | msgid "Do you like audio books? Then lets get cozy!" 42 | msgstr "ספרי שמע מעניינים אותך? בואו נתכרבל!" 43 | 44 | #: data/com.github.geigi.cozy.appdata.xml:18 45 | msgid "Cozy is a audio book player. Here are some of the features:" 46 | msgstr "Cozy הוא נגן ספרי שמע. הנה כמה מהיכולות שלו:" 47 | 48 | #: data/com.github.geigi.cozy.appdata.xml:20 49 | msgid "Import all your audio books into Cozy to browse them comfortably" 50 | msgstr "ניתן לייבא את כל הספרי השמע שלך ל־Cozy כדי לעיין בהם בנוחות" 51 | 52 | #: data/com.github.geigi.cozy.appdata.xml:21 53 | msgid "" 54 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 55 | "audio books" 56 | msgstr "" 57 | "מאפשר להאזין לספרי השמע שלך מהסוגים mp3,‏ m4b,‏ m4a (aac, ALAC, …), flac,‏ " 58 | "ogg ו־wav שאינם מוגנים ב־DRM (ניהול זכויות דיגיטליות)" 59 | 60 | #: data/com.github.geigi.cozy.appdata.xml:22 61 | msgid "Remembers your playback position" 62 | msgstr "זוכר את מיקום ההשמעה שלך" 63 | 64 | #: data/com.github.geigi.cozy.appdata.xml:23 65 | msgid "Sleep timer" 66 | msgstr "מתזמן שינה" 67 | 68 | #: data/com.github.geigi.cozy.appdata.xml:24 69 | msgid "Playback speed control for each book individually" 70 | msgstr "בקרת מהירות נגינה לכל ספר בנפרד" 71 | 72 | #: data/com.github.geigi.cozy.appdata.xml:25 73 | msgid "Search your library" 74 | msgstr "חיפוש בספרייה שלך" 75 | 76 | #: data/com.github.geigi.cozy.appdata.xml:26 77 | msgid "Multiple storage location support" 78 | msgstr "תמיכה במגוון מיקומי אחסון" 79 | 80 | #: data/com.github.geigi.cozy.appdata.xml:27 81 | msgid "" 82 | "Offline Mode! This allows you to keep an audio book on your internal storage" 83 | " if you store your audio books on an external or network drive. Perfect to " 84 | "listen to on the go!" 85 | msgstr "" 86 | "מצב בלתי מקוון! מאפשר לך לשמור על ספרי השמע באחסון הפנימי שלך במקרה שספרי " 87 | "השמע שלך מאוחסנים בכונן שמחובר לרשת או חיצוני. מושלם להאזנה בדרכים!" 88 | 89 | #: data/com.github.geigi.cozy.appdata.xml:28 90 | msgid "Drag and Drop to import new audio books" 91 | msgstr "ניתן לגרור ולשחרר כדי לייבא ספרי שמע חדשים" 92 | 93 | #: data/com.github.geigi.cozy.appdata.xml:29 94 | msgid "Sort your audio books by author, reader and name" 95 | msgstr "ניתן למיין את ספרי השמע שלך לפי סופר, קורא ושם" 96 | -------------------------------------------------------------------------------- /po/extra/hu.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Benedek Dévényi, 2024 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: extra\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 15 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 16 | "Last-Translator: Benedek Dévényi, 2024\n" 17 | "Language-Team: Hungarian (https://app.transifex.com/geigi/teams/78138/hu/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: hu\n" 22 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 23 | 24 | #: data/com.github.geigi.cozy.desktop:3 25 | msgid "Cozy" 26 | msgstr "Cozy" 27 | 28 | #: data/com.github.geigi.cozy.desktop:4 29 | msgid "Audio Book Player" 30 | msgstr "Hangoskönyv-lejátszó" 31 | 32 | #: data/com.github.geigi.cozy.desktop:5 33 | msgid "Play and organize your audio book collection" 34 | msgstr "Hallgasd és rendszerezd hangoskönyvtáradat" 35 | 36 | #: data/com.github.geigi.cozy.appdata.xml:15 37 | msgid "Listen to audio books" 38 | msgstr "Hallgass hangoskönyveket" 39 | 40 | #: data/com.github.geigi.cozy.appdata.xml:17 41 | msgid "Do you like audio books? Then lets get cozy!" 42 | msgstr "Szereted a hangoskönyveket? Akkor érezd magad otthonosan a Cozy-val!" 43 | 44 | #: data/com.github.geigi.cozy.appdata.xml:18 45 | msgid "Cozy is a audio book player. Here are some of the features:" 46 | msgstr "A Cozy egy hangoskönyv-lejátszó. Íme néhány funkciója:" 47 | 48 | #: data/com.github.geigi.cozy.appdata.xml:20 49 | msgid "Import all your audio books into Cozy to browse them comfortably" 50 | msgstr "" 51 | "Importáld hangoskönyveidet a Cozyba, hogy kényelmesen böngészhesd őket" 52 | 53 | #: data/com.github.geigi.cozy.appdata.xml:21 54 | msgid "" 55 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 56 | "audio books" 57 | msgstr "" 58 | "Hallgasd kedvenc DRM-mentes hangoskönyveidet mp3, m4b, m4a (aac, ALAC, …), " 59 | "flac, ogg és wav formátumban" 60 | 61 | #: data/com.github.geigi.cozy.appdata.xml:22 62 | msgid "Remembers your playback position" 63 | msgstr "Megjegyzi a hallgatott könyveid jelátszási pozícióját" 64 | 65 | #: data/com.github.geigi.cozy.appdata.xml:23 66 | msgid "Sleep timer" 67 | msgstr "Alvásidőzítő" 68 | 69 | #: data/com.github.geigi.cozy.appdata.xml:24 70 | msgid "Playback speed control for each book individually" 71 | msgstr "Lejátszási sebesség szabályozása minden könyvhöz külön-külön" 72 | 73 | #: data/com.github.geigi.cozy.appdata.xml:25 74 | msgid "Search your library" 75 | msgstr "Keresés a könyvtáradban" 76 | 77 | #: data/com.github.geigi.cozy.appdata.xml:26 78 | msgid "Multiple storage location support" 79 | msgstr "Támogatja több tárolóhely megadását" 80 | 81 | #: data/com.github.geigi.cozy.appdata.xml:27 82 | msgid "" 83 | "Offline Mode! This allows you to keep an audio book on your internal storage" 84 | " if you store your audio books on an external or network drive. Perfect to " 85 | "listen to on the go!" 86 | msgstr "" 87 | "Offline mód! Így letöltheted hangoskönyveidet, ha azokat egy külső vagy " 88 | "hálózati meghajtón tárolod. Tökéletes az útközbeni hallgatáshoz!" 89 | 90 | #: data/com.github.geigi.cozy.appdata.xml:28 91 | msgid "Drag and Drop to import new audio books" 92 | msgstr "Hangoskönyvek importálása „fogd és vidd” funkcióval" 93 | 94 | #: data/com.github.geigi.cozy.appdata.xml:29 95 | msgid "Sort your audio books by author, reader and name" 96 | msgstr "Rendszerezd hangoskönyveidet cím, szerző és előadó szerint" 97 | -------------------------------------------------------------------------------- /po/extra/fa_IR.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # TheBlueQuasar, 2022 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: extra\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 15 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 16 | "Last-Translator: TheBlueQuasar, 2022\n" 17 | "Language-Team: Persian (Iran) (https://app.transifex.com/geigi/teams/78138/fa_IR/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: fa_IR\n" 22 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 23 | 24 | #: data/com.github.geigi.cozy.desktop:3 25 | msgid "Cozy" 26 | msgstr "Cozy" 27 | 28 | #: data/com.github.geigi.cozy.desktop:4 29 | msgid "Audio Book Player" 30 | msgstr "پخش‌کننده کتاب صوتی" 31 | 32 | #: data/com.github.geigi.cozy.desktop:5 33 | msgid "Play and organize your audio book collection" 34 | msgstr "مجموعه کتاب‌های صوتی‌تان را پخش و مدیریت کنید" 35 | 36 | #: data/com.github.geigi.cozy.appdata.xml:15 37 | msgid "Listen to audio books" 38 | msgstr "به کتاب‌های صوتی گوش دهید" 39 | 40 | #: data/com.github.geigi.cozy.appdata.xml:17 41 | msgid "Do you like audio books? Then lets get cozy!" 42 | msgstr "آیا کتاب‌های صوتی را دوست دارید؟ پس بیایید به جای دنجی برویم!" 43 | 44 | #: data/com.github.geigi.cozy.appdata.xml:18 45 | msgid "Cozy is a audio book player. Here are some of the features:" 46 | msgstr "Cozy یک پخش‌کننده کتاب صوتی است. برخی از ویژگی‌های آن:" 47 | 48 | #: data/com.github.geigi.cozy.appdata.xml:20 49 | msgid "Import all your audio books into Cozy to browse them comfortably" 50 | msgstr "" 51 | "تمام کتاب‌های صوتی‌تان را به Cozy وارد کنید تا به‌راحتی آن‌ها را مرور نمایید" 52 | 53 | #: data/com.github.geigi.cozy.appdata.xml:21 54 | msgid "" 55 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 56 | "audio books" 57 | msgstr "" 58 | "به کتاب‌های صوتی mp3، m4a (aac، ALAV، ...)، flac، ogg و wav فاقد DRM خود " 59 | "گوش دهید" 60 | 61 | #: data/com.github.geigi.cozy.appdata.xml:22 62 | msgid "Remembers your playback position" 63 | msgstr "موقعیت پخش‌ شما را به خاطر می‌سپارد" 64 | 65 | #: data/com.github.geigi.cozy.appdata.xml:23 66 | msgid "Sleep timer" 67 | msgstr "زمان‌سنج خواب" 68 | 69 | #: data/com.github.geigi.cozy.appdata.xml:24 70 | msgid "Playback speed control for each book individually" 71 | msgstr "کنترل سرعت پخش برای هر کتاب به‌صورت مجزا" 72 | 73 | #: data/com.github.geigi.cozy.appdata.xml:25 74 | msgid "Search your library" 75 | msgstr "در کتابخانه‌تان جستجو کنید" 76 | 77 | #: data/com.github.geigi.cozy.appdata.xml:26 78 | msgid "Multiple storage location support" 79 | msgstr "پشتیبانی از چندین محل ذخیره‌سازی" 80 | 81 | #: data/com.github.geigi.cozy.appdata.xml:27 82 | msgid "" 83 | "Offline Mode! This allows you to keep an audio book on your internal storage" 84 | " if you store your audio books on an external or network drive. Perfect to " 85 | "listen to on the go!" 86 | msgstr "" 87 | "حالت آفلاین! این امکان را برایتان فراهم می‌کند که یک کتاب صوتی را در فضای " 88 | "ذخیره‌سازی داخلی‌تان نگه دارید اگر کتاب‌های صوتی‌تان را روی درایو خارجی یا " 89 | "شبکه نگه‌ می‌دارید. عالی برای گوش دادن در حال آمد و شد!" 90 | 91 | #: data/com.github.geigi.cozy.appdata.xml:28 92 | msgid "Drag and Drop to import new audio books" 93 | msgstr "برای وارد کردن کتاب‌های صوتی‌تان بکشید و بیاندازید" 94 | 95 | #: data/com.github.geigi.cozy.appdata.xml:29 96 | msgid "Sort your audio books by author, reader and name" 97 | msgstr "کتاب‌های صوتی‌تان را براساس نویسنده، گوینده و نام مرتب کنید" 98 | -------------------------------------------------------------------------------- /po/extra/cs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Pavel Zahradnik , 2021 8 | # Pavel Patz, 2022 9 | # Radim Šnajdr, 2024 10 | # 11 | #, fuzzy 12 | msgid "" 13 | msgstr "" 14 | "Project-Id-Version: extra\n" 15 | "Report-Msgid-Bugs-To: \n" 16 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 17 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 18 | "Last-Translator: Radim Šnajdr, 2024\n" 19 | "Language-Team: Czech (https://app.transifex.com/geigi/teams/78138/cs/)\n" 20 | "MIME-Version: 1.0\n" 21 | "Content-Type: text/plain; charset=UTF-8\n" 22 | "Content-Transfer-Encoding: 8bit\n" 23 | "Language: cs\n" 24 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" 25 | 26 | #: data/com.github.geigi.cozy.desktop:3 27 | msgid "Cozy" 28 | msgstr "Cozy" 29 | 30 | #: data/com.github.geigi.cozy.desktop:4 31 | msgid "Audio Book Player" 32 | msgstr "Přehrávač audioknih" 33 | 34 | #: data/com.github.geigi.cozy.desktop:5 35 | msgid "Play and organize your audio book collection" 36 | msgstr "Přehrávání a uspořádání sbírky audioknih" 37 | 38 | #: data/com.github.geigi.cozy.appdata.xml:15 39 | msgid "Listen to audio books" 40 | msgstr "Poslouchejte audioknihy" 41 | 42 | #: data/com.github.geigi.cozy.appdata.xml:17 43 | msgid "Do you like audio books? Then lets get cozy!" 44 | msgstr "Máte rádi audioknihy? Tak si je užijte s Cozy!" 45 | 46 | #: data/com.github.geigi.cozy.appdata.xml:18 47 | msgid "Cozy is a audio book player. Here are some of the features:" 48 | msgstr "Cozy je přehrávač audioknih. Mezi jeho vlastnosti patří:" 49 | 50 | #: data/com.github.geigi.cozy.appdata.xml:20 51 | msgid "Import all your audio books into Cozy to browse them comfortably" 52 | msgstr "Import všech vašich audioknih pro jejich snadné procházení" 53 | 54 | #: data/com.github.geigi.cozy.appdata.xml:21 55 | msgid "" 56 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 57 | "audio books" 58 | msgstr "" 59 | "Poslech mp3, m4b, m4a (aac, ALAC, ...), flac, ogg a wav audioknih bez DRM" 60 | 61 | #: data/com.github.geigi.cozy.appdata.xml:22 62 | msgid "Remembers your playback position" 63 | msgstr "Zapamatování pozice přehrávání" 64 | 65 | #: data/com.github.geigi.cozy.appdata.xml:23 66 | msgid "Sleep timer" 67 | msgstr "Časovač spánku" 68 | 69 | #: data/com.github.geigi.cozy.appdata.xml:24 70 | msgid "Playback speed control for each book individually" 71 | msgstr "Individuální rychlost přehrávání pro každou knihu" 72 | 73 | #: data/com.github.geigi.cozy.appdata.xml:25 74 | msgid "Search your library" 75 | msgstr "Prohledávání vaší knihovny" 76 | 77 | #: data/com.github.geigi.cozy.appdata.xml:26 78 | msgid "Multiple storage location support" 79 | msgstr "Podpora více úložných míst" 80 | 81 | #: data/com.github.geigi.cozy.appdata.xml:27 82 | msgid "" 83 | "Offline Mode! This allows you to keep an audio book on your internal storage" 84 | " if you store your audio books on an external or network drive. Perfect to " 85 | "listen to on the go!" 86 | msgstr "" 87 | "Režim offline! Ten vám umožní ponechat audioknihu v interním úložišti, pokud" 88 | " máte audioknihy uložené na externím nebo síťovém disku. Ideální pro poslech" 89 | " na cestách!" 90 | 91 | #: data/com.github.geigi.cozy.appdata.xml:28 92 | msgid "Drag and Drop to import new audio books" 93 | msgstr "Import nových audioknih přetažením" 94 | 95 | #: data/com.github.geigi.cozy.appdata.xml:29 96 | msgid "Sort your audio books by author, reader and name" 97 | msgstr "Třídění audioknih podle autora, čtenáře a názvu" 98 | -------------------------------------------------------------------------------- /po/extra/hr.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Milo Ivir , 2021 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: extra\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 15 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 16 | "Last-Translator: Milo Ivir , 2021\n" 17 | "Language-Team: Croatian (https://app.transifex.com/geigi/teams/78138/hr/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: hr\n" 22 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 23 | 24 | #: data/com.github.geigi.cozy.desktop:3 25 | msgid "Cozy" 26 | msgstr "Cozy" 27 | 28 | #: data/com.github.geigi.cozy.desktop:4 29 | msgid "Audio Book Player" 30 | msgstr "Program za slušanje audio knjiga" 31 | 32 | #: data/com.github.geigi.cozy.desktop:5 33 | msgid "Play and organize your audio book collection" 34 | msgstr "Slušaj audio knjige i upravljaj tvojom zbirkom" 35 | 36 | #: data/com.github.geigi.cozy.appdata.xml:15 37 | msgid "Listen to audio books" 38 | msgstr "Slušaj audio knjige" 39 | 40 | #: data/com.github.geigi.cozy.appdata.xml:17 41 | msgid "Do you like audio books? Then lets get cozy!" 42 | msgstr "Voliš audio knjige? Koristi Cozy!" 43 | 44 | #: data/com.github.geigi.cozy.appdata.xml:18 45 | msgid "Cozy is a audio book player. Here are some of the features:" 46 | msgstr "Cozy je player za audio knjige. Neke od funkcija su:" 47 | 48 | #: data/com.github.geigi.cozy.appdata.xml:20 49 | msgid "Import all your audio books into Cozy to browse them comfortably" 50 | msgstr "Uvezi sve svoje audio knjige u Cozy za jednostavno pregledavanje" 51 | 52 | #: data/com.github.geigi.cozy.appdata.xml:21 53 | msgid "" 54 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 55 | "audio books" 56 | msgstr "" 57 | "Slušaj audio knjige u formatima mp3, m4b, m4a (aac, ALAC, …), flac, ogg i " 58 | "wav" 59 | 60 | #: data/com.github.geigi.cozy.appdata.xml:22 61 | msgid "Remembers your playback position" 62 | msgstr "Program pamti položaj reprodukcije" 63 | 64 | #: data/com.github.geigi.cozy.appdata.xml:23 65 | msgid "Sleep timer" 66 | msgstr "Odbrojavanje do mirovanja" 67 | 68 | #: data/com.github.geigi.cozy.appdata.xml:24 69 | msgid "Playback speed control for each book individually" 70 | msgstr "Upravljanje brzinom reprodukcije za svaku knjigu pojedinačno" 71 | 72 | #: data/com.github.geigi.cozy.appdata.xml:25 73 | msgid "Search your library" 74 | msgstr "Pretraži tvoju zbirku" 75 | 76 | #: data/com.github.geigi.cozy.appdata.xml:26 77 | msgid "Multiple storage location support" 78 | msgstr "Podrška za višestruka spremišta" 79 | 80 | #: data/com.github.geigi.cozy.appdata.xml:27 81 | msgid "" 82 | "Offline Mode! This allows you to keep an audio book on your internal storage" 83 | " if you store your audio books on an external or network drive. Perfect to " 84 | "listen to on the go!" 85 | msgstr "" 86 | "Izvanmrežni način rada! Omogućuje spremanje audio knjiga u internom " 87 | "spremištu, ako se audio knjige spreme na vanjski ili mrežni pogon. Savršeno " 88 | "za slušanje na putu!" 89 | 90 | #: data/com.github.geigi.cozy.appdata.xml:28 91 | msgid "Drag and Drop to import new audio books" 92 | msgstr "Uvoz novih audio knjiga pomoću povuci-i-ispusti" 93 | 94 | #: data/com.github.geigi.cozy.appdata.xml:29 95 | msgid "Sort your audio books by author, reader and name" 96 | msgstr "Razvrstavanje audio knjiga po autoru, čitaču i imenu" 97 | -------------------------------------------------------------------------------- /po/extra/sv.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Åke Engelbrektson, 2021 8 | # Luna Jernberg , 2023 9 | # 10 | #, fuzzy 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: extra\n" 14 | "Report-Msgid-Bugs-To: \n" 15 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 16 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 17 | "Last-Translator: Luna Jernberg , 2023\n" 18 | "Language-Team: Swedish (https://app.transifex.com/geigi/teams/78138/sv/)\n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=UTF-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Language: sv\n" 23 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 24 | 25 | #: data/com.github.geigi.cozy.desktop:3 26 | msgid "Cozy" 27 | msgstr "Cozy" 28 | 29 | #: data/com.github.geigi.cozy.desktop:4 30 | msgid "Audio Book Player" 31 | msgstr "Ljudboksspelare" 32 | 33 | #: data/com.github.geigi.cozy.desktop:5 34 | msgid "Play and organize your audio book collection" 35 | msgstr "Spela upp och organisera din ljudbokssamling" 36 | 37 | #: data/com.github.geigi.cozy.appdata.xml:15 38 | msgid "Listen to audio books" 39 | msgstr "Lyssna på ljudböcker" 40 | 41 | #: data/com.github.geigi.cozy.appdata.xml:17 42 | msgid "Do you like audio books? Then lets get cozy!" 43 | msgstr "Gillar du ljudböcker? Då kan det bli mysigt!" 44 | 45 | #: data/com.github.geigi.cozy.appdata.xml:18 46 | msgid "Cozy is a audio book player. Here are some of the features:" 47 | msgstr "Cozy är en ljudboksspelare. Här är några av funktionerna:" 48 | 49 | #: data/com.github.geigi.cozy.appdata.xml:20 50 | msgid "Import all your audio books into Cozy to browse them comfortably" 51 | msgstr "" 52 | "Importera alla dina ljudböcker till Cozy för att bekvämt bläddra efter dem" 53 | 54 | #: data/com.github.geigi.cozy.appdata.xml:21 55 | msgid "" 56 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 57 | "audio books" 58 | msgstr "" 59 | "Lyssna på dina DRM-fria mp3, m4b, m4a (aac, ALAC, …), flac, ogg och wav " 60 | "ljudböcker." 61 | 62 | #: data/com.github.geigi.cozy.appdata.xml:22 63 | msgid "Remembers your playback position" 64 | msgstr "Kommer ihåg var du slutade lyssna." 65 | 66 | #: data/com.github.geigi.cozy.appdata.xml:23 67 | msgid "Sleep timer" 68 | msgstr "Insomningsur" 69 | 70 | #: data/com.github.geigi.cozy.appdata.xml:24 71 | msgid "Playback speed control for each book individually" 72 | msgstr "Kontroll av uppspelningshastighet, individuellt för varje bok." 73 | 74 | #: data/com.github.geigi.cozy.appdata.xml:25 75 | msgid "Search your library" 76 | msgstr "Sök i ditt bibliotek." 77 | 78 | #: data/com.github.geigi.cozy.appdata.xml:26 79 | msgid "Multiple storage location support" 80 | msgstr "Stöd för flera lagringsplatser." 81 | 82 | #: data/com.github.geigi.cozy.appdata.xml:27 83 | msgid "" 84 | "Offline Mode! This allows you to keep an audio book on your internal storage" 85 | " if you store your audio books on an external or network drive. Perfect to " 86 | "listen to on the go!" 87 | msgstr "" 88 | "Offlineläge! Detta gör att du kan behålla en ljudbok på ditt interna minne " 89 | "om du lagrar dina ljudböcker på en extern eller nätverksenhet. Perfekt att " 90 | "lyssna på när du är på språng!" 91 | 92 | #: data/com.github.geigi.cozy.appdata.xml:28 93 | msgid "Drag and Drop to import new audio books" 94 | msgstr "Dra och släpp för att importera nya ljudböcker" 95 | 96 | #: data/com.github.geigi.cozy.appdata.xml:29 97 | msgid "Sort your audio books by author, reader and name" 98 | msgstr "Sortera dina ljudböcker efter författare, uppläsare och namn." 99 | -------------------------------------------------------------------------------- /test/cozy/model/test_library.py: -------------------------------------------------------------------------------- 1 | from test.cozy.mocks import ApplicationSettingsMock 2 | 3 | import inject 4 | import pytest 5 | from peewee import SqliteDatabase 6 | 7 | from cozy.model.settings import Settings 8 | from cozy.settings import ApplicationSettings 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def setup_inject(peewee_database): 13 | inject.clear() 14 | inject.configure(lambda binder: (binder.bind(SqliteDatabase, peewee_database), 15 | binder.bind_to_constructor(Settings, lambda: Settings()) 16 | .bind_to_constructor(ApplicationSettings, lambda: ApplicationSettingsMock()))) 17 | yield 18 | inject.clear() 19 | 20 | 21 | def test_library_contains_books(): 22 | from cozy.model.library import Library 23 | 24 | library = Library() 25 | 26 | assert len(library.books) > 0 27 | 28 | 29 | def test_authors_contains_every_author_from_db(): 30 | from cozy.db.book import Book 31 | from cozy.model.library import Library, split_strings_to_set 32 | 33 | library = Library() 34 | books = Book.select(Book.author).distinct().order_by(Book.author) 35 | authors_from_db = [book.author for book in books] 36 | authors_from_db = list(split_strings_to_set(set(authors_from_db))) 37 | 38 | # we cannot assert the same content as the library filters books without chapters 39 | assert len(library.authors) > 0 40 | assert library.authors.issubset(authors_from_db) 41 | 42 | 43 | def test_readers_contains_every_reader_from_db(): 44 | from cozy.db.book import Book 45 | from cozy.model.library import Library, split_strings_to_set 46 | 47 | library = Library() 48 | books = Book.select(Book.reader).distinct().order_by(Book.reader) 49 | readers_from_db = [book.reader for book in books] 50 | readers_from_db = list(split_strings_to_set(set(readers_from_db))) 51 | 52 | # we cannot assert the same content as the library filters books without chapters 53 | assert len(library.readers) > 0 54 | assert library.readers.issubset(readers_from_db) 55 | 56 | 57 | def test_deleted_chapter_removed_from_lists(): 58 | from cozy.model.library import Library 59 | 60 | library = Library() 61 | 62 | chapter = next(iter(library.chapters)) 63 | library._load_all_files() 64 | library._load_all_chapters() 65 | library._on_chapter_event("chapter-deleted", next(iter(library.chapters))) 66 | 67 | assert chapter not in library.chapters 68 | assert chapter.file not in library.files 69 | 70 | 71 | def test_deleted_book_removed_from_list(): 72 | from cozy.model.library import Library 73 | 74 | library = Library() 75 | 76 | book = next(iter(library.books)) 77 | library._on_book_event("book-deleted", next(iter(library.books))) 78 | 79 | assert book not in library.books 80 | 81 | 82 | def test_rebase_path(): 83 | from cozy.model.library import Library 84 | 85 | library = Library() 86 | chapters = {chapter for chapter in library.chapters if chapter.file.startswith("20.000 Meilen unter dem Meer")} # noqa: F841 87 | library.rebase_path("20.000 Meilen unter dem Meer", "new path") 88 | 89 | 90 | def test_empty_last_book_returns_none(): 91 | from cozy.model.library import Library 92 | 93 | library = Library() 94 | library._settings.last_played_book = None 95 | 96 | assert library.last_played_book is None 97 | 98 | 99 | def test_library_last_book_returns_the_book_it_was_set_to(): 100 | from cozy.model.library import Library 101 | 102 | library = Library() 103 | library._settings.last_played_book = library.books[0] 104 | 105 | assert library.last_played_book is library.books[0] 106 | -------------------------------------------------------------------------------- /po/extra/fi.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Oi Suomi On! , 2020 8 | # Julian Geywitz , 2021 9 | # Jiri Grönroos , 2021 10 | # 11 | #, fuzzy 12 | msgid "" 13 | msgstr "" 14 | "Project-Id-Version: extra\n" 15 | "Report-Msgid-Bugs-To: \n" 16 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 17 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 18 | "Last-Translator: Jiri Grönroos , 2021\n" 19 | "Language-Team: Finnish (https://app.transifex.com/geigi/teams/78138/fi/)\n" 20 | "MIME-Version: 1.0\n" 21 | "Content-Type: text/plain; charset=UTF-8\n" 22 | "Content-Transfer-Encoding: 8bit\n" 23 | "Language: fi\n" 24 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 25 | 26 | #: data/com.github.geigi.cozy.desktop:3 27 | msgid "Cozy" 28 | msgstr "Cozy" 29 | 30 | #: data/com.github.geigi.cozy.desktop:4 31 | msgid "Audio Book Player" 32 | msgstr "Äänikirjojen toisto-ohjelma" 33 | 34 | #: data/com.github.geigi.cozy.desktop:5 35 | msgid "Play and organize your audio book collection" 36 | msgstr "Toista ja järjestele äänikirjakokoelmasi" 37 | 38 | #: data/com.github.geigi.cozy.appdata.xml:15 39 | msgid "Listen to audio books" 40 | msgstr "Kuuntele äänikirjoja" 41 | 42 | #: data/com.github.geigi.cozy.appdata.xml:17 43 | msgid "Do you like audio books? Then lets get cozy!" 44 | msgstr "Pidätkö äänikirjoista? Ota Cozy haltuun!" 45 | 46 | #: data/com.github.geigi.cozy.appdata.xml:18 47 | msgid "Cozy is a audio book player. Here are some of the features:" 48 | msgstr "Cozy on äänikirjojen toisto-ohjelma. Tässä joitain sen ominaisuuksia:" 49 | 50 | #: data/com.github.geigi.cozy.appdata.xml:20 51 | msgid "Import all your audio books into Cozy to browse them comfortably" 52 | msgstr "Tuo kaikki äänikirjasi Cozyyn selataksesi niitä mukavasti" 53 | 54 | #: data/com.github.geigi.cozy.appdata.xml:21 55 | msgid "" 56 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 57 | "audio books" 58 | msgstr "" 59 | "Kuuntele DRM-vapaita mp3, m4b (aac, ALAC, ...), flac, ogg ja wav-" 60 | "tiedostomuotoisia äänikirjojasi" 61 | 62 | #: data/com.github.geigi.cozy.appdata.xml:22 63 | msgid "Remembers your playback position" 64 | msgstr "Ohjelma muistaa kohdan johon toisto on jäänyt" 65 | 66 | #: data/com.github.geigi.cozy.appdata.xml:23 67 | msgid "Sleep timer" 68 | msgstr "Ajastin" 69 | 70 | #: data/com.github.geigi.cozy.appdata.xml:24 71 | msgid "Playback speed control for each book individually" 72 | msgstr "Toiston nopeudenhallinta jokaiselle kirjalle yksilöllisesti" 73 | 74 | #: data/com.github.geigi.cozy.appdata.xml:25 75 | msgid "Search your library" 76 | msgstr "Etsi kirjastostasi" 77 | 78 | #: data/com.github.geigi.cozy.appdata.xml:26 79 | msgid "Multiple storage location support" 80 | msgstr "Monitallennussijaintituki" 81 | 82 | #: data/com.github.geigi.cozy.appdata.xml:27 83 | msgid "" 84 | "Offline Mode! This allows you to keep an audio book on your internal storage" 85 | " if you store your audio books on an external or network drive. Perfect to " 86 | "listen to on the go!" 87 | msgstr "" 88 | "Yhteydetön tila! Tämä mahdollistaa äänikirjojesi säilyttämisen sisäisessä " 89 | "tallennustilassa mikäli varastoit äänikirjoja ulkoiseen verkkoasemaan. Sopii" 90 | " hyvin liikkuvaan kuunteluun! " 91 | 92 | #: data/com.github.geigi.cozy.appdata.xml:28 93 | msgid "Drag and Drop to import new audio books" 94 | msgstr "Raahaa ja pudota tuodaksesi kirjastoon uusia äänikirjoja" 95 | 96 | #: data/com.github.geigi.cozy.appdata.xml:29 97 | msgid "Sort your audio books by author, reader and name" 98 | msgstr "Lajittele äänikirjasi tekijän, lukijan ja nimen mukaan" 99 | -------------------------------------------------------------------------------- /po/extra/it.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Julian Geywitz , 2021 8 | # albanobattistella , 2021 9 | # 10 | #, fuzzy 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: extra\n" 14 | "Report-Msgid-Bugs-To: \n" 15 | "POT-Creation-Date: 2024-02-16 15:04+0100\n" 16 | "PO-Revision-Date: 2019-09-08 09:39+0000\n" 17 | "Last-Translator: albanobattistella , 2021\n" 18 | "Language-Team: Italian (https://app.transifex.com/geigi/teams/78138/it/)\n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=UTF-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Language: it\n" 23 | "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 24 | 25 | #: data/com.github.geigi.cozy.desktop:3 26 | msgid "Cozy" 27 | msgstr "Cozy" 28 | 29 | #: data/com.github.geigi.cozy.desktop:4 30 | msgid "Audio Book Player" 31 | msgstr "Lettore di audiolibri" 32 | 33 | #: data/com.github.geigi.cozy.desktop:5 34 | msgid "Play and organize your audio book collection" 35 | msgstr "Riproduci e organizza la tua raccolta di audiolibri" 36 | 37 | #: data/com.github.geigi.cozy.appdata.xml:15 38 | msgid "Listen to audio books" 39 | msgstr "Ascolta gli audiolibri" 40 | 41 | #: data/com.github.geigi.cozy.appdata.xml:17 42 | msgid "Do you like audio books? Then lets get cozy!" 43 | msgstr "Ti piacciono gli audiolibri? Allora mettiamoci comodi!" 44 | 45 | #: data/com.github.geigi.cozy.appdata.xml:18 46 | msgid "Cozy is a audio book player. Here are some of the features:" 47 | msgstr "Cosy è un lettore di audiolibri. Ecco alcune delle caratteristiche:" 48 | 49 | #: data/com.github.geigi.cozy.appdata.xml:20 50 | msgid "Import all your audio books into Cozy to browse them comfortably" 51 | msgstr "Importa tutti i tuoi audiolibri in Cosy per sfogliarli comodamente" 52 | 53 | #: data/com.github.geigi.cozy.appdata.xml:21 54 | msgid "" 55 | "Listen to your DRM free mp3, m4b, m4a (aac, ALAC, …), flac, ogg and wav " 56 | "audio books" 57 | msgstr "" 58 | "Ascolta i tuoi audiolibri DRM free mp3, m4b, m4a (aac, ALAC, ...), flac, ogg" 59 | " e wav" 60 | 61 | #: data/com.github.geigi.cozy.appdata.xml:22 62 | msgid "Remembers your playback position" 63 | msgstr "Ricorda la posizione di riproduzione" 64 | 65 | #: data/com.github.geigi.cozy.appdata.xml:23 66 | msgid "Sleep timer" 67 | msgstr "Timer" 68 | 69 | #: data/com.github.geigi.cozy.appdata.xml:24 70 | msgid "Playback speed control for each book individually" 71 | msgstr "" 72 | "Controllo della velocità di riproduzione per ogni libro individualmente" 73 | 74 | #: data/com.github.geigi.cozy.appdata.xml:25 75 | msgid "Search your library" 76 | msgstr "Cerca nella tua libreria" 77 | 78 | #: data/com.github.geigi.cozy.appdata.xml:26 79 | msgid "Multiple storage location support" 80 | msgstr "Supporto per più posizioni di archiviazione" 81 | 82 | #: data/com.github.geigi.cozy.appdata.xml:27 83 | msgid "" 84 | "Offline Mode! This allows you to keep an audio book on your internal storage" 85 | " if you store your audio books on an external or network drive. Perfect to " 86 | "listen to on the go!" 87 | msgstr "" 88 | "Modalità offline! Ciò ti consente di conservare un audiolibro nella memoria " 89 | "interna se archivi i tuoi audiolibri su un'unità esterna o di rete. Perfetto" 90 | " da ascoltare in movimento!" 91 | 92 | #: data/com.github.geigi.cozy.appdata.xml:28 93 | msgid "Drag and Drop to import new audio books" 94 | msgstr "Trascina e rilascia per importare nuovi audio libri" 95 | 96 | #: data/com.github.geigi.cozy.appdata.xml:29 97 | msgid "Sort your audio books by author, reader and name" 98 | msgstr "Ordina i tuoi audiolibri per autore, lettore e nome" 99 | --------------------------------------------------------------------------------