├── 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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------