first commit
Some checks are pending
spot-quality / ci-check (push) Waiting to run
spot-quality / shellcheck (push) Waiting to run

This commit is contained in:
Théo Barnouin 2024-11-13 16:41:51 +01:00
commit 15cf412840
255 changed files with 47845 additions and 0 deletions

12
.editorconfig Normal file
View file

@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
[*.{build,yml,ui,yaml,css,blp}]
indent_size = 2

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.po diff=po

39
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,39 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**General information:**
- Distribution:
- Installation method [e. g. built from source, installed from Flathub...]:
- Version [e.g. 0.1.0]:
- Device used [e. g. desktop, phone...]:
**Stack trace:**
If applicable, run the application from a terminal and paste relevant log output.
```
flatpak run --env=RUST_BACKTRACE=full dev.alextren.Spot
```
**Additional context**
Add any other context about the problem here.

11
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

22
.github/workflows/spot-development.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: spot-development
on:
pull_request:
branches: [development]
workflow_dispatch:
jobs:
flatpak-builder:
name: "Flatpak Builder"
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-nightly
options: --privileged
steps:
- uses: actions/checkout@v2
- uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4
with:
bundle: "spot.flatpak"
manifest-path: "dev.alextren.Spot.development.json"
cache-key: flatpak-builder-${{ github.sha }}
run-tests: true

36
.github/workflows/spot-quality.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: spot-quality
on:
push:
pull_request:
jobs:
ci-check:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
components: rustfmt
- name: Add empty config.rs
run: |
echo >> src/config.rs
- name: Format
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
shellcheck:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- run: |
sudo apt-get -y update && sudo apt-get -y install shellcheck
find $GITHUB_WORKSPACE -type f -and \( -name "*.sh" \) | xargs shellcheck

24
.github/workflows/spot-snapshots.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: spot-snapshots
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
flatpak-builder:
name: "Flatpak Builder"
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-42
options: --privileged
steps:
- uses: actions/checkout@v2
- uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4
with:
bundle: "spot.flatpak"
manifest-path: "dev.alextren.Spot.snapshots.json"
cache-key: flatpak-builder-${{ github.sha }}
run-tests: true

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
/target/
/doc/target
/cargo/
/build/
/.flatpak-builder/
/src/config.rs
/subprojects/blueprint-compiler

51
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,51 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"options": {
"env": {
"RUST_BACKTRACE": "1",
"RUST_LOG": "spot=debug",
"LANG": "C",
//"https_proxy": "localhost:8080"
}
},
"tasks": [
{
"label": "meson",
"type": "shell",
"command": "meson setup target -Dbuildtype=debug -Doffline=false --prefix=\"$HOME/.local\""
},
{
"label": "build",
"type": "shell",
"command": "ninja install -C target",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"clear": true
}
},
{
"label": "run",
"type": "shell",
"command": "$HOME/.local/bin/spot",
"presentation": {
"reveal": "always",
"clear": true
}
},
{
"label": "test",
"type": "shell",
"command": "meson test -C target --verbose",
"group": {
"kind": "test",
"isDefault": true
}
}
]
}

2
ARTISTS Normal file
View file

@ -0,0 +1,2 @@
Tobias Bernard
Noëlle

22
AUTHORS Normal file
View file

@ -0,0 +1,22 @@
Alexandre Trendel
Noëlle
Armin Begert
Douile
Diego Augusto
TotalDarkness-NRF
Seioo Inoue
xRMG412
realJavabot
Gabriele Musco
Alistair Francis
Daniel Peukert
Nils Tonnätt
Niklas Sauter
Nicolas Fella
Fridolin Weisser
Jan Przebor
Warren Hu
bbb651
Julius Rüberg
janbrummer
Alisson Lauffer

5651
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

78
Cargo.toml Normal file
View file

@ -0,0 +1,78 @@
[package]
name = "spot"
version = "0.5.0"
edition = "2018"
license = "MIT"
[dependencies.gtk]
version = "^0.6.6"
package = "gtk4"
features = ["gnome_44", "blueprint"]
[dependencies.libadwaita]
version = "^0.4.1"
features = ["v1_2"]
[dependencies.gdk]
version = "^0.6.3"
package = "gdk4"
[dependencies.gio]
version = "^0.17.9"
features = ["v2_60"]
[dependencies.glib]
version = "^0.17.9"
features = ["v2_60"]
[dependencies.librespot]
version = "0.5.0"
features = ["alsa-backend", "pulseaudio-backend", "gstreamer-backend"]
[dependencies.tokio]
version = "1"
features = ["rt", "macros", "sync"]
[dependencies.futures]
package = "futures"
version = "0.3.18"
[dependencies.serde]
version = "^1.0.136"
features = ["derive"]
[dependencies.serde_json]
version = "^1.0.96"
[dependencies.isahc]
version = "^1.7.2"
features = ["json"]
[dependencies.rand]
version = "^0.8.5"
features = ["small_rng"]
[dependencies.gettext-rs]
version = "0.7.0"
features = ["gettext-system"]
[dependencies.secret-service]
version = "3.0.1"
features = ["rt-async-io-crypto-rust"]
[dependencies]
gdk-pixbuf = "0.17.0"
ref_filter_map = "1.0.1"
regex = "1.8.3"
async-std = "1.12.0"
form_urlencoded = "1.0.1"
zbus = "3.13"
zvariant = "3.14"
thiserror = "1.0.40"
lazy_static = "1.4.0"
log = "0.4.17"
env_logger = "0.10.0"
percent-encoding = "2.2.0"
oauth2 = "4.4"
url = "2.4.1"
open = "5.3.0"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Alexandre Trendel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

150
README.md Normal file
View file

@ -0,0 +1,150 @@
# Spot [![spot-snapshots](https://github.com/xou816/spot/actions/workflows/spot-snapshots.yml/badge.svg?branch=master)](https://github.com/xou816/spot/actions/workflows/spot-snapshots.yml)
Gtk/Rust native Spotify client for the GNOME desktop. **Only works with premium accounts!**
Based on [librespot](https://github.com/librespot-org/librespot/).
Join the discussion on [Matrix](https://matrix.to/#/#spot-devel:matrix.org).
![Spot screenshot](./data/appstream/2.png)
## Installing
<a href='https://flathub.org/apps/details/dev.alextren.Spot'><img width='130' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a>
## Usage notes
### Credentials
It is recommended to install a libsecret compliant keyring application, such as [GNOME Keyring](https://wiki.gnome.org/action/show/Projects/GnomeKeyring) (aka seahorse). This will allow saving your password securely between launches.
In GNOME, things should work out of the box. It might be a bit trickier to get it working in other DEs: see this [ArchWiki entry](https://wiki.archlinux.org/index.php/GNOME/Keyring) for detailed explanations on how to automatically start the daemon with your session.
Bear special attention to the fact that to enable automatic login, you might have to use the same password for your user account and for the keyring, and that the keyring might need to be [set as default](https://wiki.archlinux.org/index.php/GNOME/Keyring#Passwords_are_not_remembered).
See [this comment](https://github.com/xou816/spot/issues/92#issuecomment-801852593) for more details!
### Login in with Facebook
...is not supported. However, you can update your account in order to be able to log in with a username and password [as explained in this issue](https://github.com/xou816/spot/issues/373).
### Settings
Spot can also be configured via `gsettings` if you want to change the audio backend, the song bitrate, etc.
### Seek bar warping
It is possible to click on the seek bar to navigate to that position in a song. If you are having issues with this not working you may have [gtk-primary-button-warps-slider](https://docs.gtk.org/gtk3/property.Settings.gtk-primary-button-warps-slider.html) set to false.
In order to fix this issue set the value to true in your gtk configuration.
### Scrobbling
Scrobbling is not supported directly by Spot. However, you can use a tool such a [rescrobbled](https://github.com/InputUsername/rescrobbled) ([see #85](https://github.com/xou816/spot/issues/85)).
### Lyrics
Similarly, Spot does not display lyrics for songs, but you can use [osdlyrics](https://github.com/osdlyrics/osdlyrics) ([see #226](https://github.com/xou816/spot/issues/226)).
### Gtk theme
Spot uses the dark theme variant by default; this can be changed in the settings.
If you are using the flatpak version, don't forget to install your theme with flatpak as well. See [this comment](https://github.com/xou816/spot/issues/209#issuecomment-860180537) for details.
Similarly, snap also requires that you install the corresponding snap for your theme. See [this comment](https://github.com/xou816/spot/issues/338#issuecomment-975543476) for details.
## Features
**Only works with premium accounts!**
- playback control (play/pause, prev/next, seeking, shuffle, repeat (none, all, song))
- selection mode: easily browse and select mutliple tracks to queue them
- browse your saved albums and playlists
- search albums and artists
- view an artist's releases
- view users' playlists
- view album info
- credentials management with Secret Service
- MPRIS integration
- playlist management (creation and edition)
- liked tracks
### Planned
- GNOME search provider?
- improved search? (track results)
- recommendations?
## Contributing
Contributions are welcome! If you wish, add yourself to the `AUTHORS` files when submitting your contribution.
For any large feature/change, **please** open an issue first to discuss implementation and design decisions.
### Translating
Translations are managed using `gettext` and are available in the `po/` subdirectory.
**Please use [POEditor](https://poeditor.com/join/project?hash=xfVrpQfRBM) to submit translations.**
If you feel like it, you are welcome to open a PR to be added to the `TRANSLATORS` file!
## Building
### With GNOME Builder and flatpak
Pre-requisite: install the `org.freedesktop.Sdk.Extension.rust-stable` SDK extension with flatpak. Builder might do this for you automatically, but it will install an older version; make sure the version installed matches the version of the Freedesktop SDK GNOME uses.
Open the project in GNOME Builder and make the `dev.alextren.Spot.development.json` configuration active. Then build :)
### Manually
Requires Rust (stable), **GTK4**, and a couple other things. Also requires **libadwaita** and **blueprint-compiler**: they are not packaged on all distros at the moment, you might have to build them yourself!
With meson:
```
meson target -Dbuildtype=debug -Doffline=false --prefix="$HOME/.local"
ninja install -C target
# to run test/linter/etc
meson test -C target --verbose
```
This will install a `.desktop` file among other things, and the spot executable will be put in `.local/bin` (you might want to add it to your path).
To build an optimized release build, use `-Dbuildtype=release` instead.
### Regenerating potfiles
When adding new `msgids`, don't forget to regenerate/update the potfiles.
```
ninja spot-pot -C target
ninja spot-update-po -C target
```
### Pulling updated strings from POEditor
We are now using POEditor and the wonderful [`poeditor-sync`](https://github.com/mick88/poeditor-sync) tool.
```
poeditor pull
```
### Regenerating sources for flatpak
Using [flatpak-cargo-generator.py](https://github.com/flatpak/flatpak-builder-tools/tree/master/cargo):
```
ninja cargo-sources.json -C target
```
### Debugging
Set the `RUST_LOG` env variable to the appropriate level.
Debug builds (flatpak) are available from the master branch on Github (see the `spot-snaphots` action).
Spot caches images and HTTP responses in `~/.cache/spot`.
Spot uses [isahc](https://github.com/sagebind/isahc), which uses libcurl, therefore you can set the `https_proxy` env variable to help with debugging. In debug mode, Spot skips SSL certificate verification.

25
TRANSLATORS Normal file
View file

@ -0,0 +1,25 @@
Heimen Stoffels
Philipp Kiemle
kleinHeiti
Ondřej Sluka
Ícar N. S.
Jonasz Potoniec
José Miguel Sarasola
Hugo Meireles Gonçalves
Paul Bragin
Yusuf Çınar Özmen
Kukuh Syafaat
Lucas Araujo
Igor Dyatlov
Jiri Grönroos
Guilherme
Lars Martinsen
Sergio
SoftInterlingua
Tine Jozelj
Ana Pika Šubic
Amor Ali
Seioo Inoue
Dmitry
Julius Rüberg
Francesco Babetto

View file

@ -0,0 +1,450 @@
#!/usr/bin/env python3
__license__ = 'MIT'
import json
from urllib.parse import urlparse, ParseResult, parse_qs
import os
import contextlib
import copy
import glob
import subprocess
import argparse
import logging
import hashlib
import asyncio
from typing import Any, Dict, List, NamedTuple, Optional, Tuple, TypedDict
import aiohttp
import toml
CRATES_IO = 'https://static.crates.io/crates'
CARGO_HOME = 'cargo'
CARGO_CRATES = f'{CARGO_HOME}/vendor'
VENDORED_SOURCES = 'vendored-sources'
GIT_CACHE = 'flatpak-cargo/git'
COMMIT_LEN = 7
@contextlib.contextmanager
def workdir(path: str):
oldpath = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(oldpath)
def canonical_url(url: str) -> ParseResult:
'Converts a string to a Cargo Canonical URL, as per https://github.com/rust-lang/cargo/blob/35c55a93200c84a4de4627f1770f76a8ad268a39/src/cargo/util/canonical_url.rs#L19'
# Hrm. The upstream cargo does not replace those URLs, but if we don't then it doesn't work too well :(
url = url.replace('git+https://', 'https://')
u = urlparse(url)
# It seems cargo drops query and fragment
u = ParseResult(u.scheme, u.netloc, u.path, '', '', '')
u = u._replace(path = u.path.rstrip('/'))
if u.netloc == 'github.com':
u = u._replace(scheme = 'https')
u = u._replace(path = u.path.lower())
if u.path.endswith('.git'):
u = u._replace(path = u.path[:-len('.git')])
return u
def get_git_tarball(repo_url: str, commit: str) -> str:
url = canonical_url(repo_url)
path = url.path.split('/')[1:]
assert len(path) == 2
owner = path[0]
if path[1].endswith('.git'):
repo = path[1].replace('.git', '')
else:
repo = path[1]
if url.hostname == 'github.com':
return f'https://codeload.{url.hostname}/{owner}/{repo}/tar.gz/{commit}'
elif url.hostname.split('.')[0] == 'gitlab': # type: ignore
return f'https://{url.hostname}/{owner}/{repo}/-/archive/{commit}/{repo}-{commit}.tar.gz'
elif url.hostname == 'bitbucket.org':
return f'https://{url.hostname}/{owner}/{repo}/get/{commit}.tar.gz'
else:
raise ValueError(f'Don\'t know how to get tarball for {repo_url}')
async def get_remote_sha256(url: str) -> str:
logging.info(f"started sha256({url})")
sha256 = hashlib.sha256()
async with aiohttp.ClientSession(raise_for_status=True) as http_session:
async with http_session.get(url) as response:
while True:
data = await response.content.read(4096)
if not data:
break
sha256.update(data)
logging.info(f"done sha256({url})")
return sha256.hexdigest()
_TomlType = Dict[str, Any]
def load_toml(tomlfile: str = 'Cargo.lock') -> _TomlType:
with open(tomlfile, 'r') as f:
toml_data = toml.load(f)
return toml_data
def git_repo_name(git_url: str, commit: str) -> str:
name = canonical_url(git_url).path.split('/')[-1]
return f'{name}-{commit[:COMMIT_LEN]}'
def fetch_git_repo(git_url: str, commit: str) -> str:
repo_dir = git_url.replace('://', '_').replace('/', '_')
cache_dir = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
clone_dir = os.path.join(cache_dir, 'flatpak-cargo', repo_dir)
if not os.path.isdir(os.path.join(clone_dir, '.git')):
subprocess.run(['git', 'clone', '--depth=1', git_url, clone_dir], check=True)
rev_parse_proc = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=clone_dir, check=True,
stdout=subprocess.PIPE)
head = rev_parse_proc.stdout.decode().strip()
if head[:COMMIT_LEN] != commit[:COMMIT_LEN]:
subprocess.run(['git', 'fetch', 'origin', commit], cwd=clone_dir, check=True)
subprocess.run(['git', 'checkout', commit], cwd=clone_dir, check=True)
return clone_dir
class _GitPackage(NamedTuple):
path: str
package: _TomlType
workspace: Optional[_TomlType]
@property
def normalized(self) -> _TomlType:
package = copy.deepcopy(self.package)
if self.workspace is None:
return package
for section_key, section in package.items():
# XXX We ignore top-level lists here; maybe we should iterate over list items, too
if not isinstance(section, dict):
continue
for key, value in section.items():
if not isinstance(value, dict):
continue
if not value.get('workspace'):
continue
package[section_key][key] = self.workspace[section_key][key]
return package
_GitPackagesType = Dict[str, _GitPackage]
async def get_git_repo_packages(git_url: str, commit: str) -> _GitPackagesType:
logging.info('Loading packages from %s', git_url)
git_repo_dir = fetch_git_repo(git_url, commit)
packages: _GitPackagesType = {}
with workdir(git_repo_dir):
if os.path.isfile('Cargo.toml'):
packages.update(await get_cargo_toml_packages(load_toml('Cargo.toml'), '.'))
else:
for toml_path in glob.glob('*/Cargo.toml'):
packages.update(await get_cargo_toml_packages(load_toml(toml_path),
os.path.dirname(toml_path)))
assert packages, f"No packages found in {git_repo_dir}"
logging.debug(
'Packages in %s:\n%s',
git_url,
json.dumps(
{k: v.path for k, v in packages.items()},
indent=4,
),
)
return packages
async def get_cargo_toml_packages(root_toml: _TomlType, root_dir: str) -> _GitPackagesType:
assert not os.path.isabs(root_dir) and os.path.isdir(root_dir)
assert 'package' in root_toml or 'workspace' in root_toml
packages: _GitPackagesType = {}
async def get_dep_packages(
entry: _TomlType,
toml_dir: str,
workspace: Optional[_TomlType] = None,
):
assert not os.path.isabs(toml_dir)
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
if 'dependencies' in entry:
for dep_name, dep in entry['dependencies'].items():
if 'package' in dep:
dep_name = dep['package']
if 'path' not in dep:
continue
if dep_name in packages:
continue
dep_dir = os.path.normpath(os.path.join(toml_dir, dep['path']))
logging.debug("Loading dependency %s from %s", dep_name, dep_dir)
dep_toml = load_toml(os.path.join(dep_dir, 'Cargo.toml'))
assert dep_toml['package']['name'] == dep_name, toml_dir
await get_dep_packages(dep_toml, dep_dir, workspace)
packages[dep_name] = _GitPackage(
path=dep_dir,
package=dep,
workspace=workspace,
)
if 'target' in entry:
for _, target in entry['target'].items():
await get_dep_packages(target, toml_dir)
if 'package' in root_toml:
await get_dep_packages(root_toml, root_dir)
packages[root_toml['package']['name']] = _GitPackage(
path=root_dir,
package=root_toml,
workspace=None,
)
if 'workspace' in root_toml:
for member in root_toml['workspace'].get('members', []):
for subpkg_toml in glob.glob(os.path.join(root_dir, member, 'Cargo.toml')):
subpkg = os.path.normpath(os.path.dirname(subpkg_toml))
logging.debug(
"Loading workspace member %s in %s",
subpkg_toml,
os.path.abspath(root_dir),
)
pkg_toml = load_toml(subpkg_toml)
await get_dep_packages(pkg_toml, subpkg, root_toml['workspace'])
packages[pkg_toml['package']['name']] = _GitPackage(
path=subpkg,
package=pkg_toml,
workspace=root_toml['workspace'],
)
return packages
_FlatpakSourceType = Dict[str, Any]
async def get_git_repo_sources(
url: str,
commit: str,
tarball: bool = False,
) -> List[_FlatpakSourceType]:
name = git_repo_name(url, commit)
if tarball:
tarball_url = get_git_tarball(url, commit)
git_repo_sources = [{
'type': 'archive',
'archive-type': 'tar-gzip',
'url': tarball_url,
'sha256': await get_remote_sha256(tarball_url),
'dest': f'{GIT_CACHE}/{name}',
}]
else:
git_repo_sources = [{
'type': 'git',
'url': url,
'commit': commit,
'dest': f'{GIT_CACHE}/{name}',
}]
return git_repo_sources
_GitRepo = TypedDict('_GitRepo', {'lock': asyncio.Lock, 'commits': Dict[str, _GitPackagesType]})
_GitReposType = Dict[str, _GitRepo]
_VendorEntryType = Dict[str, Dict[str, str]]
async def get_git_package_sources(
package: _TomlType,
git_repos: _GitReposType,
) -> Tuple[List[_FlatpakSourceType], _VendorEntryType]:
name = package['name']
source = package['source']
commit = urlparse(source).fragment
assert commit, 'The commit needs to be indicated in the fragement part'
canonical = canonical_url(source)
repo_url = canonical.geturl()
git_repo = git_repos.setdefault(repo_url, {
'commits': {},
'lock': asyncio.Lock(),
})
async with git_repo['lock']:
if commit not in git_repo['commits']:
git_repo['commits'][commit] = await get_git_repo_packages(repo_url, commit)
cargo_vendored_entry: _VendorEntryType = {
repo_url: {
'git': repo_url,
'replace-with': VENDORED_SOURCES,
}
}
rev = parse_qs(urlparse(source).query).get('rev')
tag = parse_qs(urlparse(source).query).get('tag')
branch = parse_qs(urlparse(source).query).get('branch')
if rev:
assert len(rev) == 1
cargo_vendored_entry[repo_url]['rev'] = rev[0]
elif tag:
assert len(tag) == 1
cargo_vendored_entry[repo_url]['tag'] = tag[0]
elif branch:
assert len(branch) == 1
cargo_vendored_entry[repo_url]['branch'] = branch[0]
logging.info("Adding package %s from %s", name, repo_url)
git_pkg = git_repo['commits'][commit][name]
pkg_repo_dir = os.path.join(GIT_CACHE, git_repo_name(repo_url, commit), git_pkg.path)
git_sources: List[_FlatpakSourceType] = [
{
'type': 'shell',
'commands': [
f'cp -r --reflink=auto "{pkg_repo_dir}" "{CARGO_CRATES}/{name}"'
],
},
{
'type': 'inline',
'contents': toml.dumps(git_pkg.normalized),
'dest': f'{CARGO_CRATES}/{name}', #-{version}',
'dest-filename': 'Cargo.toml',
},
{
'type': 'inline',
'contents': json.dumps({'package': None, 'files': {}}),
'dest': f'{CARGO_CRATES}/{name}', #-{version}',
'dest-filename': '.cargo-checksum.json',
}
]
return (git_sources, cargo_vendored_entry)
async def get_package_sources(
package: _TomlType,
cargo_lock: _TomlType,
git_repos: _GitReposType,
) -> Optional[Tuple[List[_FlatpakSourceType], _VendorEntryType]]:
metadata = cargo_lock.get('metadata')
name = package['name']
version = package['version']
if 'source' not in package:
logging.debug('%s has no source', name)
return None
source = package['source']
if source.startswith('git+'):
return await get_git_package_sources(package, git_repos)
key = f'checksum {name} {version} ({source})'
if metadata is not None and key in metadata:
checksum = metadata[key]
elif 'checksum' in package:
checksum = package['checksum']
else:
logging.warning(f'{name} doesn\'t have checksum')
return None
crate_sources = [
{
'type': 'archive',
'archive-type': 'tar-gzip',
'url': f'{CRATES_IO}/{name}/{name}-{version}.crate',
'sha256': checksum,
'dest': f'{CARGO_CRATES}/{name}-{version}',
},
{
'type': 'inline',
'contents': json.dumps({'package': checksum, 'files': {}}),
'dest': f'{CARGO_CRATES}/{name}-{version}',
'dest-filename': '.cargo-checksum.json',
},
]
return (crate_sources, {'crates-io': {'replace-with': VENDORED_SOURCES}})
async def generate_sources(
cargo_lock: _TomlType,
git_tarballs: bool = False,
) -> List[_FlatpakSourceType]:
# {
# "git-repo-url": {
# "lock": asyncio.Lock(),
# "commits": {
# "commit-hash": {
# "package-name": "./relative/package/path"
# }
# }
# }
# }
git_repos: _GitReposType = {}
sources: List[_FlatpakSourceType] = []
package_sources = []
cargo_vendored_sources = {
VENDORED_SOURCES: {'directory': f'{CARGO_CRATES}'},
}
pkg_coros = [get_package_sources(p, cargo_lock, git_repos) for p in cargo_lock['package']]
for pkg in await asyncio.gather(*pkg_coros):
if pkg is None:
continue
else:
pkg_sources, cargo_vendored_entry = pkg
package_sources.extend(pkg_sources)
cargo_vendored_sources.update(cargo_vendored_entry)
logging.debug('Adding collected git repos:\n%s', json.dumps(list(git_repos), indent=4))
git_repo_coros = []
for git_url, git_repo in git_repos.items():
for git_commit in git_repo['commits']:
git_repo_coros.append(get_git_repo_sources(git_url, git_commit, git_tarballs))
sources.extend(sum(await asyncio.gather(*git_repo_coros), []))
sources.extend(package_sources)
logging.debug('Vendored sources:\n%s', json.dumps(cargo_vendored_sources, indent=4))
sources.append({
'type': 'inline',
'contents': toml.dumps({
'source': cargo_vendored_sources,
}),
'dest': CARGO_HOME,
'dest-filename': 'config'
})
return sources
def main():
parser = argparse.ArgumentParser()
parser.add_argument('cargo_lock', help='Path to the Cargo.lock file')
parser.add_argument('-o', '--output', required=False, help='Where to write generated sources')
parser.add_argument('-t', '--git-tarballs', action='store_true', help='Download git repos as tarballs')
parser.add_argument('-d', '--debug', action='store_true')
args = parser.parse_args()
if args.output is not None:
outfile = args.output
else:
outfile = 'generated-sources.json'
if args.debug:
loglevel = logging.DEBUG
else:
loglevel = logging.INFO
logging.basicConfig(level=loglevel)
generated_sources = asyncio.run(generate_sources(load_toml(args.cargo_lock),
git_tarballs=args.git_tarballs))
with open(outfile, 'w') as out:
json.dump(generated_sources, out, indent=4, sort_keys=False)
if __name__ == '__main__':
main()

6898
cargo-sources.json Normal file

File diff suppressed because it is too large Load diff

BIN
data/appstream/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
data/appstream/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
data/appstream/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,356 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>dev.alextren.Spot</id>
<url type="homepage">https://github.com/xou816/spot</url>
<name>Spot</name>
<summary>Listen to music on Spotify</summary>
<launchable type="desktop-id">dev.alextren.Spot.desktop</launchable>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<content_rating type="oars-1.1" />
<requires>
<display_length compare="ge">360</display_length>
</requires>
<recommends>
<control>keyboard</control>
<control>pointing</control>
<control>touch</control>
</recommends>
<!-- see https://puri.sm/posts/specify-form-factors-in-your-librem-5-apps/ -->
<custom>
<value key="Purism::form_factor">workstation</value>
<value key="Purism::form_factor">mobile</value>
</custom>
<description>
<p>
Listen to music on Spotify.
<b>Requires a premium account.</b>
</p>
<p>Current features:</p>
<ul>
<li>playback control (play/pause, prev/next, seeking)</li>
<li>play queue with shuffle option</li>
<li>selection mode: easily browse and select mutliple tracks to queue them</li>
<li>browse your saved albums and playlists</li>
<li>search albums and artists</li>
<li>view an artist's releases</li>
<li>view users' playlists</li>
<li>credentials management with Secret Service</li>
<li>MPRIS integration</li>
</ul>
</description>
<screenshots>
<screenshot>
<image>https://raw.githubusercontent.com/xou816/spot/master/data/appstream/1.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/xou816/spot/master/data/appstream/2.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/xou816/spot/master/data/appstream/3.png</image>
</screenshot>
</screenshots>
<release version="0.4.0" date="2023-02-24">
<description>
<p>
It's been a while, hasn't it? I'm back, but please allow everyone some time to work through the issues and pull requests :)
</p>
<ul>
<li>added an option for gapless playback (thanks @supremesnickers!)</li>
<li>fixed theme management (thanks @ctsk!)</li>
<li>fixed the behaviour of the "previous" button for consistency with other media players (thanks @SomewhereOutInSpace!)</li>
<li>the user will now be prompted to unlock the keyring if needed (thanks @rainDiX!)</li>
<li>added playlist creation and edition</li>
<li>more bug fixes</li>
</ul>
<p>
Thanks to all the contributors who are somehow still around :) If you feel like you've been unjustly forgotten from the AUTHORS or TRANSLATORS file, feel free to open a PR.
</p>
</description>
</release>
<release version="0.3.3" date="2022-03-29">
<description>
<p>
What's new:
</p>
<ul>
<li>login dialog now uses a password entry (thanks @Toorero!)</li>
<li>fix a startup crash with some locales</li>
</ul>
</description>
</release>
<release version="0.3.2" date="2022-03-27">
<description>
<p>
New features:
</p>
<ul>
<li>save tracks to library</li>
<li>basic Spotify URI handling (thanks @bbb651!)</li>
<li>settings dialog (thanks @sei0o!)</li>
<li>brand new icon (thanks @bertob!)</li>
<li>bug fixes</li>
</ul>
<p>
Thank you to everyone involved in this release!
</p>
</description>
</release>
<releases>
<release version="0.3.1" date="2022-01-23">
<description>
<p>Bugfix release:</p>
<ul>
<li>fix a startup crash with some locales (thanks @jakubiszon26!)</li>
<li>fix a crash when attempting to view some users' profile (thanks @juxuanu!)</li>
</ul>
</description>
</release>
<release version="0.3.0" date="2022-01-13">
<description>
<p>
There has been no release for a while, but this one packs quite a few features:
</p>
<ul>
<li>redesigned several parts of the application: headers, selection tools, library, album view... (thanks @jannuary!)</li>
<li>many, many, many more subtle design changes and tweaks still owed to @jannuary</li>
<li>playlists are now accessible from the sidebar (thanks @abegert!)</li>
<li>the album art is visible for individual songs, and the release year is visible for albums (thanks @abegert and @sei0o!)</li>
<li>volume control over MPRIS (thanks @Diegovsky!)</li>
<li>as always, updated translations for yet more languages; thanks to the many people involved on POEditor!</li>
</ul>
<p>
Thank you so much to everyone involved for this release, including those I could not mention. If you're willing to contribute, do not hesitate to seek advice from those trusted contributors, and be sure to direct your UX questions towards @jannuary (as long as they're fine with it!).
</p>
</description>
</release>
<release version="0.2.2" date="2021-11-10">
<description>
<p>Quick fix for a startup crash, sorry for the inconvenience. Thanks @xRMG412! </p>
</description>
</release>
<release version="0.2.1" date="2021-10-17">
<description>
<p>
What's new:
</p>
<ul>
<li>browse saved tracks from Spot</li>
<li>a status page is now displayed when no albums or playlists have been added (thanks @Diegovsky!)</li>
<li>change the access point port from GSettings; this should help users running Spot behind a firewall (thanks @sei0o!)</li>
<li>display a warning in the login dialog if Caps Lock is enabled (thanks @przebor!)</li>
<li>added Purism form-factor metadata; this should improve the discoverability of Spot (thanks @1peter10!), and generally improved the appstream metadata to match the updated guidelines</li>
<li>various bugfixes (clear credentials on auth failure, load all tracks for long albums...)</li>
</ul>
<p>Thank you to the many contributors to this release, as well as to all those contributing translations on POEditor!</p>
</description>
</release>
<release version="0.2.0" date="2021-08-24">
<description>
<p>
What's new:
</p>
<ul>
<li>ported app to GTK4 and libadwaita</li>
<li>MPRIS: handle shuffling and loop status</li>
<li>improved album size on small screens</li>
</ul>
</description>
</release>
<release version="0.1.16" date="2021-08-17">
<description>
<p>
What's new:
</p>
<ul>
<li>added a new album info modal with full release details (thanks TotalDarkness-NRF!)</li>
<li>improved player controls: added the ability to repeat a single track or the whole queue (thanks TotalDarkness-NRF!)</li>
<li>improved login credentials management: (re)use auth token when possible instead of always using email and password (thanks nn1ks!)</li>
<li>MPRIS fixes: raising the player is now properly implemented (thanks eladyn!)</li>
<li>...actually fix an issue with playlists not being modifiable when login in with email</li>
</ul>
<p>
Special thanks to TotalDarkness-NRF for contributing several major features for this version.
</p>
</description>
</release>
<release version="0.1.15" date="2021-06-30">
<description>
<p>
What's new:
</p>
<ul>
<li>ability to remove tracks from writable playlists</li>
<li>improved login error handling</li>
<li>fixed an issue with playlists not being modifiable when login in with email</li>
<li>MPRIS: the desktop entry for Spot is now properly referenced (thanks nicolasfella!)</li>
<li>quality: various clippy fixes (thanks nn1ks!)</li>
<li>new translations: Turkish, Indonesian and Brazilian Portuguese (thanks YusufOzmen01, cho2 and lucraraujo, as well as ondras12345 for the reviews!)</li>
<li></li>
</ul>
</description>
</release>
<release version="0.1.14" date="2021-05-09">
<description>
<p>
What's new:
</p>
<ul>
<li>long playlists are now handled (somewhat) properly, although this has some drawbacks (shuffling isn't so random...)</li>
<li>the MPRIS implementation now supports seeking, and should report the proper album art and album name; thanks Douile!</li>
<li>selection tools now allow adding to a playlist (not yet removing, though)</li>
<li>Russian translation</li>
</ul>
<p>
Thanks to all contributors, translators and bug reporters!
</p>
</description>
</release>
<release version="0.1.13" date="2021-04-12">
<description>
<p>
What's new:
</p>
<ul>
<li>new selection tools: move tracks up and down in queue, quickly select multiple tracks from current view...</li>
<li>touch and hold a song list (or right click) to enter selection mode</li>
<li>Portuguese translation</li>
<li>fixed session restoration requiring manually repeating last action after a long period of inactivity</li>
<li>fixed parsing for playlists with local tracks</li>
</ul>
</description>
</release>
<release version="0.1.12" date="2021-03-24">
<description>
<p>
What's new:
</p>
<ul>
<li>browse users' playlists by clicking on their name in playlists you follow (thanks a bunch, Douile!)</li>
<li>Catalan, Czech, Polish and Spanish translations; thanks to all the translators involved!</li>
<li>bug fixes</li>
</ul>
</description>
</release>
<release version="0.1.11" date="2021-03-17">
<description>
<p>
This release fixes the broken "Search" icon, and adds French, German and Dutch translations. Thanks to all translators for their contribution!
</p>
</description>
</release>
<release version="0.1.10" date="2021-03-13">
<description>
<p>
New features:
</p>
<ul>
<li>redesigned seekbar, login dialog and search (type from any screen to start a search)</li>
<li>song durations in playlist widgets (thanks realJavabot!)</li>
<li>play queue management (queue/dequeue/clear queue)</li>
<li>selection mode: enter selection mode to easily queue multiple tracks</li>
<li>keyboards shortcuts</li>
<li>various playback options through GSettings (no GUI yet)</li>
</ul>
</description>
</release>
<release version="0.1.9" date="2021-03-02">
<description>
<p>
New feature: contextual menus for songs are now available everywhere, allowing you to easily navigate to related artists or share tracks. Thanks Douile!
</p>
<p>
This release also includes numerous fixes, including a few crashes, performance issues, and most importantly playlists being truncated.
</p>
</description>
</release>
<release version="0.1.8" date="2021-02-20">
<description>
<p>
New feature: the main window can now be closed without stopping playback. Use Quit or Ctrl+Q to exit the app.
</p>
<p>
This release fixes numerous issues, including: main window being too large and too tall for phones, startup crash for playlist without images, "About" dialog not closing in qtile, keyboard navigation being broken in the login dialog. Special thanks to Douile for contributing fixes for many of these isssues!
</p>
</description>
</release>
<release version="0.1.7" date="2021-02-15">
<description>
<p>
Hotfix for a crash in search results
</p>
</description>
</release>
<release version="0.1.6" date="2021-02-15">
<description>
<p>
New features:
</p>
<ul>
<li>search results now include artists</li>
<li>albums can be saved and unsaved from the library</li>
</ul>
</description>
</release>
<release version="0.1.5" date="2021-02-11">
<description>
<p>
New features:
</p>
<ul>
<li>browse saved playlists</li>
<li>quick access to "Now playing"</li>
<li>artists top tracks</li>
<li>API token renewal</li>
</ul>
</description>
</release>
<release version="0.1.4" date="2021-02-04">
<description>
<p>
New features:
</p>
<ul>
<li>adaptive UI with libhandy</li>
</ul>
</description>
</release>
<release version="0.1.3" date="2021-01-31">
<description>
<p>
New features:
</p>
<ul>
<li>minimal MPRIS integration</li>
<li>"About" dialog</li>
</ul>
</description>
</release>
<release version="0.1.2" date="2021-01-25">
<description>
<p>
New features:
</p>
<ul>
<li>improved playlist widget, added menu to jump from "Now playing" to related albums</li>
<li>added in-app notifications for some errors</li>
</ul>
<p>Fixes:</p>
<ul>
<li>fixed an issue where songs would be skipped in a playlist when autoplaying</li>
<li>fixed character encoding in search queries</li>
</ul>
</description>
</release>
<release version="0.1.1" date="2021-01-17">
<description>
<p>Notables changes: symbolic icons everyhere (thanks gabmus!), shuffle play, logout</p>
</description>
</release>
<release version="0.1" date="2021-01-11">
<description>
<p>Initial release</p>
</description>
</release>
</releases>
</component>

View file

@ -0,0 +1,13 @@
[Desktop Entry]
Version=1.5
Name=Spot
Exec=spot %u
GenericName=Music Player
Icon=dev.alextren.Spot
Terminal=false
Type=Application
Categories=GTK;GNOME;Music;AudioVideo;
MimeType=x-scheme-handler/spotify;
StartupNotify=true
X-Purism-FormFactor=Workstation;Mobile;
SingleMainWindow=true

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="spot">
<enum id="dev.alextren.Spot.AudioBackend">
<value value="0" nick="pulseaudio" />
<value value="1" nick="alsa" />
<value value="2" nick="gstreamer" />
</enum>
<enum id="dev.alextren.Spot.Bitrate">
<value value="0" nick="96" />
<value value="1" nick="160" />
<value value="2" nick="320" />
</enum>
<enum id="dev.alextren.Spot.ThemePref">
<value value="0" nick="light" />
<value value="1" nick="dark" />
<value value="2" nick="system" />
</enum>
<schema id="dev.alextren.Spot" path="/dev/alextren/Spot/">
<key name='theme-preference' enum='dev.alextren.Spot.ThemePref'>
<default>'system'</default>
<summary>The theme preference</summary>
</key>
<key name="window-width" type="i">
<default>1080</default>
<summary>The width of the window</summary>
</key>
<key name="window-height" type="i">
<default>720</default>
<summary>The height of the window</summary>
</key>
<key name="window-is-maximized" type="b">
<default>false</default>
<summary>A flag to enable maximized mode</summary>
</key>
<key name='player-bitrate' enum='dev.alextren.Spot.Bitrate'>
<default>'160'</default>
<summary>Songs bitrate (96, 160, 320kbps)</summary>
</key>
<key name='audio-backend' enum='dev.alextren.Spot.AudioBackend'>
<default>'pulseaudio'</default>
<summary>Audio backend</summary>
</key>
<key name="gapless-playback" type="b">
<default>true</default>
<summary>A flag to enable gap-less playback</summary>
</key>
<key name='alsa-device' type='s'>
<default>'default'</default>
<summary>Alsa device (if audio backend is 'alsa')</summary>
</key>
<key name='ap-port' type='u'>
<default>0</default>
<summary>Port to communicate with Spotify's server (access point). Setting to 0 (default) allows Spot to use servers running on any port.</summary>
</key>
</schema>
</schemalist>

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<linearGradient id="a" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3d3846"/>
<stop offset="0.5" stop-color="#77767b"/>
<stop offset="1" stop-color="#5e5c64"/>
</linearGradient>
<linearGradient id="b" x1="27.798828" x2="27.798828" xlink:href="#a" y1="70" y2="64"/>
<linearGradient id="c" gradientUnits="userSpaceOnUse" x1="28.000011" x2="100.000011" y1="58.749985" y2="58.749985">
<stop offset="0" stop-color="#3d3846"/>
<stop offset="0.5" stop-color="#9a9996"/>
<stop offset="1" stop-color="#5e5c64"/>
</linearGradient>
<linearGradient id="d" gradientTransform="matrix(0.5 -0.866025 0.866025 0.5 0 -172)" gradientUnits="userSpaceOnUse" x1="-232.630707" x2="-160.630707" y1="187.425629" y2="187.425629">
<stop offset="0" stop-color="#9a9996"/>
<stop offset="0.5" stop-color="#9a9996"/>
<stop offset="1" stop-color="#3d3846"/>
</linearGradient>
<linearGradient id="e" gradientTransform="matrix(-1.066666 0 0 -1.066666 132.266647 377.866577)" x1="34" x2="94" xlink:href="#a" y1="268" y2="268"/>
<linearGradient id="f" gradientTransform="matrix(1.066666 0 0 -1.066666 -12.799976 200.799957)" gradientUnits="userSpaceOnUse" x1="72" x2="72" y1="120" y2="88">
<stop offset="0" stop-color="#8ff0a4"/>
<stop offset="1" stop-color="#33d17a"/>
</linearGradient>
<linearGradient id="g" gradientUnits="userSpaceOnUse" x1="24.000004" x2="62.55785" y1="224.554276" y2="174.025513">
<stop offset="0" stop-color="#9a9996"/>
<stop offset="0.587814" stop-color="#c0bfbc"/>
<stop offset="1" stop-color="#9a9996"/>
</linearGradient>
<filter id="h" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="i">
<g filter="url(#h)">
<rect fill-opacity="0.3" height="128" width="128"/>
</g>
</mask>
<clipPath id="j">
<rect height="152" width="192"/>
</clipPath>
<path d="m 21 64 h 86 c 1.65625 0 3 1.34375 3 3 s -1.34375 3 -3 3 h -86 c -1.65625 0 -3 -1.34375 -3 -3 s 1.34375 -3 3 -3 z m 0 0" fill="url(#b)"/>
<path d="m 64 12 c -19.882812 0 -36 16.117188 -36 36 v 44 h 72 v -44 c 0 -19.882812 -16.117188 -36 -36 -36 z m 0 0" fill="url(#c)"/>
<path d="m 82 60.824219 c 17.21875 9.941406 23.117188 31.957031 13.175781 49.175781 c -9.941406 17.21875 -31.957031 23.117188 -49.175781 13.175781 c -17.21875 -9.941406 -23.117188 -31.957031 -13.175781 -49.175781 c 9.941406 -17.21875 31.957031 -23.117188 49.175781 -13.175781 z m 0 0" fill="url(#d)"/>
<path d="m 32 92 c 0 -17.671875 14.328125 -32 32 -32 s 32 14.328125 32 32 s -14.328125 32 -32 32 s -32 -14.328125 -32 -32 z m 0 0" fill="url(#e)"/>
<path d="m 64 113.332031 c -15.597656 -0.019531 -28.90625 -11.285156 -31.507812 -26.664062 c 2.601562 -15.378907 15.910156 -26.644531 31.507812 -26.667969 c 15.636719 0.019531 28.96875 11.339844 31.527344 26.765625 c -2.644532 15.347656 -15.953125 26.5625 -31.527344 26.566406 z m 0 0" fill="url(#f)"/>
<path d="m 24 244 v -30 c 0 -22.089844 17.910156 -40 40 -40 s 40 17.910156 40 40 v 30" fill="none" stroke="url(#g)" stroke-linecap="round" stroke-width="4" transform="matrix(1 0 0 1 0 -172)"/>
<g fill="#241f31">
<path d="m 65.5 20 c 0 0.828125 -0.671875 1.5 -1.5 1.5 s -1.5 -0.671875 -1.5 -1.5 s 0.671875 -1.5 1.5 -1.5 s 1.5 0.671875 1.5 1.5 z m 0 0"/>
<path d="m 59.167969 20.554688 c 0 0.828124 -0.671875 1.5 -1.5 1.5 s -1.5 -0.671876 -1.5 -1.5 c 0 -0.828126 0.671875 -1.5 1.5 -1.5 s 1.5 0.671874 1.5 1.5 z m 0 0"/>
<path d="m 52.9375 22.210938 c 0 0.832031 -0.625 1.5 -1.390625 1.5 c -0.769531 0 -1.394531 -0.667969 -1.394531 -1.5 c 0 -0.828126 0.625 -1.5 1.394531 -1.5 c 0.765625 0 1.390625 0.671874 1.390625 1.5 z m 0 0"/>
<path d="m 47.632812 24.605469 c 0 0.828125 -0.566406 1.5 -1.261718 1.5 c -0.699219 0 -1.261719 -0.671875 -1.261719 -1.5 s 0.5625 -1.5 1.261719 -1.5 c 0.695312 0 1.261718 0.671875 1.261718 1.5 z m 0 0"/>
<path d="m 43.167969 27.589844 c 0 0.828125 -0.574219 1.5 -1.28125 1.5 s -1.28125 -0.671875 -1.28125 -1.5 s 0.574219 -1.5 1.28125 -1.5 s 1.28125 0.671875 1.28125 1.5 z m 0 0"/>
<path d="m 39.304688 30.9375 c 0 0.828125 -0.511719 1.5 -1.144532 1.5 c -0.632812 0 -1.148437 -0.671875 -1.148437 -1.5 c 0 -0.832031 0.515625 -1.5 1.148437 -1.5 c 0.632813 0 1.144532 0.667969 1.144532 1.5 z m 0 0"/>
<path d="m 35.816406 34.984375 c 0 0.828125 -0.46875 1.5 -1.046875 1.5 s -1.046875 -0.671875 -1.046875 -1.5 s 0.46875 -1.5 1.046875 -1.5 s 1.046875 0.671875 1.046875 1.5 z m 0 0"/>
<path d="m 32.996094 39.222656 c 0 0.828125 -0.382813 1.5 -0.855469 1.5 s -0.855469 -0.671875 -0.855469 -1.5 c 0 -0.832031 0.382813 -1.5 0.855469 -1.5 s 0.855469 0.667969 0.855469 1.5 z m 0 0"/>
<path d="m 30.769531 43.972656 c 0 0.828125 -0.316406 1.5 -0.710937 1.5 c -0.394532 0 -0.710938 -0.671875 -0.710938 -1.5 s 0.316406 -1.5 0.710938 -1.5 c 0.394531 0 0.710937 0.671875 0.710937 1.5 z m 0 0"/>
<path d="m 29.226562 48.8125 c 0 0.828125 -0.230468 1.5 -0.507812 1.5 c -0.28125 0 -0.507812 -0.671875 -0.507812 -1.5 s 0.226562 -1.5 0.507812 -1.5 c 0.277344 0 0.507812 0.671875 0.507812 1.5 z m 0 0"/>
<path d="m 68.832031 20.554688 c 0 0.828124 0.671875 1.5 1.5 1.5 s 1.5 -0.671876 1.5 -1.5 c 0 -0.828126 -0.671875 -1.5 -1.5 -1.5 s -1.5 0.671874 -1.5 1.5 z m 0 0"/>
<path d="m 75.0625 22.210938 c 0 0.832031 0.625 1.5 1.390625 1.5 c 0.769531 0 1.394531 -0.667969 1.394531 -1.5 c 0 -0.828126 -0.625 -1.5 -1.394531 -1.5 c -0.765625 0 -1.390625 0.671874 -1.390625 1.5 z m 0 0"/>
<path d="m 80.367188 24.605469 c 0 0.828125 0.566406 1.5 1.261718 1.5 c 0.699219 0 1.261719 -0.671875 1.261719 -1.5 s -0.5625 -1.5 -1.261719 -1.5 c -0.695312 0 -1.261718 0.671875 -1.261718 1.5 z m 0 0"/>
<path d="m 84.832031 27.589844 c 0 0.828125 0.574219 1.5 1.28125 1.5 s 1.28125 -0.671875 1.28125 -1.5 s -0.574219 -1.5 -1.28125 -1.5 s -1.28125 0.671875 -1.28125 1.5 z m 0 0"/>
<path d="m 88.695312 30.9375 c 0 0.828125 0.511719 1.5 1.144532 1.5 c 0.632812 0 1.148437 -0.671875 1.148437 -1.5 c 0 -0.832031 -0.515625 -1.5 -1.148437 -1.5 c -0.632813 0 -1.144532 0.667969 -1.144532 1.5 z m 0 0"/>
<path d="m 92.183594 34.984375 c 0 0.828125 0.46875 1.5 1.046875 1.5 s 1.046875 -0.671875 1.046875 -1.5 s -0.46875 -1.5 -1.046875 -1.5 s -1.046875 0.671875 -1.046875 1.5 z m 0 0"/>
<path d="m 95.003906 39.222656 c 0 0.828125 0.382813 1.5 0.855469 1.5 s 0.855469 -0.671875 0.855469 -1.5 c 0 -0.832031 -0.382813 -1.5 -0.855469 -1.5 s -0.855469 0.667969 -0.855469 1.5 z m 0 0"/>
<path d="m 97.230469 43.972656 c 0 0.828125 0.316406 1.5 0.710937 1.5 c 0.394532 0 0.710938 -0.671875 0.710938 -1.5 s -0.316406 -1.5 -0.710938 -1.5 c -0.394531 0 -0.710937 0.671875 -0.710937 1.5 z m 0 0"/>
<path d="m 98.773438 48.8125 c 0 0.828125 0.230468 1.5 0.507812 1.5 c 0.28125 0 0.507812 -0.671875 0.507812 -1.5 s -0.226562 -1.5 -0.507812 -1.5 c -0.277344 0 -0.507812 0.671875 -0.507812 1.5 z m 0 0"/>
<path d="m 65.5 25 c 0 0.828125 -0.671875 1.5 -1.5 1.5 s -1.5 -0.671875 -1.5 -1.5 s 0.671875 -1.5 1.5 -1.5 s 1.5 0.671875 1.5 1.5 z m 0 0"/>
<path d="m 59.167969 25.554688 c 0 0.828124 -0.671875 1.5 -1.5 1.5 s -1.5 -0.671876 -1.5 -1.5 c 0 -0.828126 0.671875 -1.5 1.5 -1.5 s 1.5 0.671874 1.5 1.5 z m 0 0"/>
<path d="m 52.9375 27.210938 c 0 0.832031 -0.625 1.5 -1.390625 1.5 c -0.769531 0 -1.394531 -0.667969 -1.394531 -1.5 c 0 -0.828126 0.625 -1.5 1.394531 -1.5 c 0.765625 0 1.390625 0.671874 1.390625 1.5 z m 0 0"/>
<path d="m 47.632812 29.605469 c 0 0.828125 -0.566406 1.5 -1.261718 1.5 c -0.699219 0 -1.261719 -0.671875 -1.261719 -1.5 s 0.5625 -1.5 1.261719 -1.5 c 0.695312 0 1.261718 0.671875 1.261718 1.5 z m 0 0"/>
<path d="m 43.167969 32.589844 c 0 0.828125 -0.574219 1.5 -1.28125 1.5 s -1.28125 -0.671875 -1.28125 -1.5 s 0.574219 -1.5 1.28125 -1.5 s 1.28125 0.671875 1.28125 1.5 z m 0 0"/>
<path d="m 39.304688 35.9375 c 0 0.828125 -0.511719 1.5 -1.144532 1.5 c -0.632812 0 -1.148437 -0.671875 -1.148437 -1.5 c 0 -0.832031 0.515625 -1.5 1.148437 -1.5 c 0.632813 0 1.144532 0.667969 1.144532 1.5 z m 0 0"/>
<path d="m 35.816406 39.984375 c 0 0.828125 -0.46875 1.5 -1.046875 1.5 s -1.046875 -0.671875 -1.046875 -1.5 s 0.46875 -1.5 1.046875 -1.5 s 1.046875 0.671875 1.046875 1.5 z m 0 0"/>
<path d="m 32.996094 44.222656 c 0 0.828125 -0.382813 1.5 -0.855469 1.5 s -0.855469 -0.671875 -0.855469 -1.5 c 0 -0.832031 0.382813 -1.5 0.855469 -1.5 s 0.855469 0.667969 0.855469 1.5 z m 0 0"/>
<path d="m 30.769531 48.972656 c 0 0.828125 -0.316406 1.5 -0.710937 1.5 c -0.394532 0 -0.710938 -0.671875 -0.710938 -1.5 s 0.316406 -1.5 0.710938 -1.5 c 0.394531 0 0.710937 0.671875 0.710937 1.5 z m 0 0"/>
<path d="m 29.226562 53.8125 c 0 0.828125 -0.230468 1.5 -0.507812 1.5 c -0.28125 0 -0.507812 -0.671875 -0.507812 -1.5 s 0.226562 -1.5 0.507812 -1.5 c 0.277344 0 0.507812 0.671875 0.507812 1.5 z m 0 0"/>
<path d="m 68.832031 25.554688 c 0 0.828124 0.671875 1.5 1.5 1.5 s 1.5 -0.671876 1.5 -1.5 c 0 -0.828126 -0.671875 -1.5 -1.5 -1.5 s -1.5 0.671874 -1.5 1.5 z m 0 0"/>
<path d="m 75.0625 27.210938 c 0 0.832031 0.625 1.5 1.390625 1.5 c 0.769531 0 1.394531 -0.667969 1.394531 -1.5 c 0 -0.828126 -0.625 -1.5 -1.394531 -1.5 c -0.765625 0 -1.390625 0.671874 -1.390625 1.5 z m 0 0"/>
<path d="m 80.367188 29.605469 c 0 0.828125 0.566406 1.5 1.261718 1.5 c 0.699219 0 1.261719 -0.671875 1.261719 -1.5 s -0.5625 -1.5 -1.261719 -1.5 c -0.695312 0 -1.261718 0.671875 -1.261718 1.5 z m 0 0"/>
<path d="m 84.832031 32.589844 c 0 0.828125 0.574219 1.5 1.28125 1.5 s 1.28125 -0.671875 1.28125 -1.5 s -0.574219 -1.5 -1.28125 -1.5 s -1.28125 0.671875 -1.28125 1.5 z m 0 0"/>
<path d="m 88.695312 35.9375 c 0 0.828125 0.511719 1.5 1.144532 1.5 c 0.632812 0 1.148437 -0.671875 1.148437 -1.5 c 0 -0.832031 -0.515625 -1.5 -1.148437 -1.5 c -0.632813 0 -1.144532 0.667969 -1.144532 1.5 z m 0 0"/>
<path d="m 92.183594 39.984375 c 0 0.828125 0.46875 1.5 1.046875 1.5 s 1.046875 -0.671875 1.046875 -1.5 s -0.46875 -1.5 -1.046875 -1.5 s -1.046875 0.671875 -1.046875 1.5 z m 0 0"/>
<path d="m 95.003906 44.222656 c 0 0.828125 0.382813 1.5 0.855469 1.5 s 0.855469 -0.671875 0.855469 -1.5 c 0 -0.832031 -0.382813 -1.5 -0.855469 -1.5 s -0.855469 0.667969 -0.855469 1.5 z m 0 0"/>
<path d="m 97.230469 48.972656 c 0 0.828125 0.316406 1.5 0.710937 1.5 c 0.394532 0 0.710938 -0.671875 0.710938 -1.5 s -0.316406 -1.5 -0.710938 -1.5 c -0.394531 0 -0.710937 0.671875 -0.710937 1.5 z m 0 0"/>
<path d="m 98.773438 53.8125 c 0 0.828125 0.230468 1.5 0.507812 1.5 c 0.28125 0 0.507812 -0.671875 0.507812 -1.5 s -0.226562 -1.5 -0.507812 -1.5 c -0.277344 0 -0.507812 0.671875 -0.507812 1.5 z m 0 0"/>
</g>
<g clip-path="url(#j)" mask="url(#i)" transform="matrix(1 0 0 1 -8 -16)">
<path d="m 72 66 c -19.882812 0 -36 16.117188 -36 36 v 2 c 0 -19.882812 16.117188 -36 36 -36 s 36 16.117188 36 36 v -2 c 0 -19.882812 -16.117188 -36 -36 -36 z m 0 0" fill="#241f31"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<rect fill-opacity="0.3" height="16" width="16"/>
</g>
</mask>
<clipPath id="c">
<rect height="152" width="192"/>
</clipPath>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -168 -16)">
<path d="m 72 66 c -19.882812 0 -36 16.117188 -36 36 v 2 c 0 -19.882812 16.117188 -36 36 -36 s 36 16.117188 36 36 v -2 c 0 -19.882812 -16.117188 -36 -36 -36 z m 0 0" fill="#241f31"/>
</g>
<g fill="#241f31">
<path d="m 3 3.855469 l -2.84375 4.925781 c -0.2304688 0.398438 -0.09375 0.90625 0.304688 1.132812 l 7.941406 4.585938 l 4 -6.929688 l -4.402344 -2.542968 v 4.140625 c 0 1 -0.832031 1.832031 -1.828125 1.832031 h -1.339844 c -1 0 -1.832031 -0.832031 -1.832031 -1.832031 z m 10.269531 4.214843 l -4 6.929688 l 1.011719 0.585938 c 0.398438 0.230468 0.90625 0.09375 1.136719 -0.304688 l 3.167969 -5.492188 c 0.230468 -0.394531 0.09375 -0.902343 -0.304688 -1.132812 z m 0 0"/>
<path d="m 4.832031 0 c -0.460937 0 -0.832031 0.371094 -0.832031 0.832031 v 8.335938 c 0 0.460937 0.371094 0.832031 0.832031 0.832031 h 1.335938 c 0.460937 0 0.832031 -0.371094 0.832031 -0.832031 v -8.335938 c 0 -0.460937 -0.371094 -0.832031 -0.832031 -0.832031 z m 0.667969 7.5 c 0.277344 0 0.5 0.222656 0.5 0.5 s -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 s 0.222656 -0.5 0.5 -0.5 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

31
data/meson.build Normal file
View file

@ -0,0 +1,31 @@
install_data('dev.alextren.Spot.desktop',
install_dir: get_option('datadir') / 'applications'
)
install_subdir('hicolor',
install_dir: get_option('datadir') / 'icons'
)
install_data('dev.alextren.Spot.appdata.xml',
install_dir: get_option('datadir') / 'appdata'
)
install_data('dev.alextren.Spot.gschema.xml',
install_dir: get_option('datadir') / 'glib-2.0/schemas'
)
compile_schemas = find_program('glib-compile-schemas', required: true)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
args: ['--strict', '--dry-run', meson.current_source_dir()]
)
endif
appstream_util = find_program('appstream-util', required: false)
if appstream_util.found()
test(
'Validate appstream appdata',
appstream_util,
args: ['validate-relax', meson.current_source_dir() / 'dev.alextren.Spot.appdata.xml']
)
endif

View file

@ -0,0 +1,68 @@
{
"app-id": "dev.alextren.Spot",
"runtime": "org.gnome.Platform",
"runtime-version": "master",
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command": "spot",
"finish-args": [
"--share=network",
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland",
"--socket=pulseaudio",
"--device=dri",
"--talk-name=org.freedesktop.secrets",
"--own-name=org.mpris.MediaPlayer2.Spot"
],
"separate-locales": false,
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin",
"env": {
"RUST_BACKTRACE": "full",
"RUST_LOG": "spot=debug"
}
},
"cleanup": [
"/include",
"/lib/pkgconfig",
"/man",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
"*.la",
"*.a"
],
"modules": [
{
"name": "blueprint-compiler",
"buildsystem": "meson",
"sources": [
{
"type": "archive",
"url": "https://gitlab.gnome.org/jwestman/blueprint-compiler/-/archive/v0.8.1/blueprint-compiler-v0.8.1.tar.gz",
"sha256": "9207697cfac6e87a3c0ccf463be1a95c3bd06aa017c966a7e352ad5bc486cf3c"
}
]
},
{
"name": "spot",
"builddir": true,
"buildsystem": "meson",
"config-opts": [
"-Doffline=true",
"-Dbuildtype=debug"
],
"sources": [
{
"type": "dir",
"path": "."
},
"cargo-sources.json"
]
}
]
}

View file

@ -0,0 +1,71 @@
{
"app-id": "dev.alextren.Spot",
"runtime": "org.gnome.Platform",
"runtime-version": "44",
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command": "spot",
"finish-args": [
"--share=network",
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland",
"--socket=pulseaudio",
"--device=dri",
"--talk-name=org.freedesktop.secrets",
"--own-name=org.mpris.MediaPlayer2.Spot"
],
"separate-locales": false,
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin",
"env": {
"CARGO_HOME": "/run/build/spot/cargo",
"RUST_BACKTRACE": "full",
"RUST_LOG": "spot=debug"
},
"strip": false,
"no-debuginfo": true
},
"cleanup": [
"/include",
"/lib/pkgconfig",
"/man",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
"*.la",
"*.a"
],
"modules": [
{
"name": "blueprint-compiler",
"buildsystem": "meson",
"sources": [
{
"type": "archive",
"url": "https://gitlab.gnome.org/jwestman/blueprint-compiler/-/archive/v0.8.1/blueprint-compiler-v0.8.1.tar.gz",
"sha256": "9207697cfac6e87a3c0ccf463be1a95c3bd06aa017c966a7e352ad5bc486cf3c"
}
]
},
{
"name": "spot",
"builddir": true,
"buildsystem": "meson",
"config-opts": [
"-Doffline=true",
"-Dbuildtype=debug"
],
"sources": [
{
"type": "dir",
"path": "."
},
"cargo-sources.json"
]
}
]
}

4
doc/.latexmkrc Normal file
View file

@ -0,0 +1,4 @@
$pdf_mode = 1;
$pdf_previewer = '';
$pdflatex = 'lualatex -interaction=nonstopmode -synctex=1 -shell-escape %O %S';
$out_dir = 'target';

4
doc/Dockerfile Normal file
View file

@ -0,0 +1,4 @@
FROM atrendel/doxerlive:15-basic
RUN apk add gettext py3-pygments
ADD Makefile /var/doxerlive/
RUN make install

30
doc/Makefile Normal file
View file

@ -0,0 +1,30 @@
all: install build
build: doc.tex
latexmk -c doc.tex
watch: doc.tex
latexmk -pvc doc.tex
DEPENDENCIES = luatex $\
fontspec $\
lm $\
xcolor $\
xcolor-solarized $\
fontawesome $\
xifthen $\
ifmtarg $\
pgf $\
pgf-blur $\
ec $\
etoolbox $\
xkeyval $\
minted kvoptions fancyvrb fvextra upquote float ifplatform pdftexcmds xstring lineno framed catchfile
install:
tlmgr update --self
tlmgr update texlive-scripts
tlmgr install $(DEPENDENCIES)
clean:
latexmk -C

BIN
doc/doc.pdf Normal file

Binary file not shown.

191
doc/doc.tex Normal file
View file

@ -0,0 +1,191 @@
\documentclass[12pt, a4paper]{article}
\usepackage[margin=25mm]{geometry}
\usepackage{tikz}
\usepackage[outputdir=target]{minted}
\usemintedstyle{lovelace}
\renewcommand\familydefault{\sfdefault}
\definecolor{spotifygreen}{HTML}{1DB954}
\usetikzlibrary{calc}
\usetikzlibrary{arrows.meta}
\usetikzlibrary{decorations.pathreplacing}
\usetikzlibrary{shapes}
\usetikzlibrary{positioning}
\tikzstyle{box}=[fill=black, solid, line width=0mm, text=white, rounded corners=2mm, font={\bfseries}, inner sep=3mm]
\tikzstyle{sizedbox}=[box, text width=3cm, anchor=north, align=center]
\tikzstyle{link}=[->, >=latex, line width=.5mm, rounded corners, shorten >=1pt]
\tikzstyle{smalllink}=[->, >=latex, line width=.2mm, rounded corners, shorten >=1pt, dashed, gray]
\begin{document}
\section{Data flow within Spot}
\subsection{Overview}
\paragraph{Single source of truth.} There is a single place that is considered the source of truth for anything that is related to the app state, and that is, well, the \texttt{AppState}. The app state aggregates the state of the UI, as well as the player state. This makes it easier to keep things in sync -- when possible, anything state-related should be read from the app state over some local, possibly out-of-date state.
\paragraph{Centralized.} That state is centralized and unique. This allows various parts of the application to access any part of it, and conversely makes it easy to perform state updates that affect various and sometimes unrelated parts of the application.
\paragraph{Controlled mutations.} There is only one way to modify the app state, and that is by dispatching \emph{actions} -- plain structs that represent a mutation to the state. Updates to the state produce \emph{events}, which \texttt{EventListeners} can use to update the UI.
\begin{figure}[!h]
\centering
\begin{tikzpicture}
\node[box, fill=spotifygreen, minimum height=2cm] (model) at (0, 0) {\ttfamily AppModel};
\node[box] (ui) at (-4, -3) {Gtk widgets};
\node[box] (listeners) at (4, -3) {Listeners};
\draw[link] (ui) edge[bend left=20] node[above, sloped] {\footnotesize actions} (model);
\draw[link] (model) edge[bend left=10] node[above, sloped] {\footnotesize events} (listeners);
\draw[link] (listeners) edge[bend left=60] node[below, sloped] {\footnotesize update} (ui);
\draw[link, dashed] (listeners) edge[bend left=10] node[below, sloped] {\footnotesize read-only access} (model);
\end{tikzpicture}
\caption{The data flow and and its relation to the UI -- the \texttt{AppModel} enforces read-only access to the state.}
\label{fig:flow}
\end{figure}
This draws heavy inspiration from the Flux architecture\footnote{See https://facebook.github.io/flux/docs/in-depth-overview for instance}; the one big difference here is that there is no way to automatically find out which portion of the UI should be updated. Instead, listeners are responsible for figuring out the updates to apply based on the events.
It should be noted that the app state is only readable from the main thread for simplicity.
\subsection{How actions are handled}
Here is the relevant part of the code\footnote{Variables have been renamed for clarity...} related to handling actions and notifying listeners:
\begin{minted}{rust}
let events = self.model.update_state(action);
for event in events.iter() {
for listener in self.listeners.iter_mut() {
listener.on_event(event);
}
}
\end{minted}
That first line is the only time that the app state is borrowed mutably -- to apply actions.
On the technical side: all actions being dispatched, synchronous or not, are eventually sent through a \texttt{futures::channel::mpsc} channel. The consumer on the other end of the channel is a future that will be executed by GLib. This allows Gtk to process \emph{all actions} at its own pace, as part of its main loop.
Note: futures are used a lot in the code to perform asynchronous operations such as calls to the Spotify API. To ease the use of futures, the dispatcher allows working with asynchronous actions, that is, futures that output one or more actions. Again, these futures are eventually handled in the main Gtk loop.
\subsection{A listener: the player subsystem}
Any element that wishes to update the state or react to changes from the state has to follow that same pattern. For instance, the ``player'' part of Spot receives \texttt{Commands} (mapped from events by a \texttt{PlayerNotifier}) to start playing music, and dispatches actions back the app through a \texttt{SpotifyPlayerDelegate} (see figure \ref{fig:player}).
These two extra elements add some indirection so that the player is not too strongly coupled to the rest of the app (it does not and should not care about most events, afterall!). Moreover, those commands are handled in a separate thread where the player lives.
\begin{figure}[!ht]
\centering
\begin{tikzpicture}
\node[box, fill=spotifygreen, minimum height=2cm] (model) at (0, 0) {\ttfamily AppModel};
\draw[smalllink] (-5, -3) node[sizedbox] (listeners) {Components}
-- +(0, -1.5) node[right] {update}
-- +(0, -2) node[sizedbox] (ui) {Gtk widgets};
\draw[smalllink] (5, -3) node[sizedbox, fill=gray] (notifier) {\ttfamily PlayerNotifier}
-- +(0, -1.5) node[right] {command}
-- +(0, -2) node[sizedbox, fill=gray] (player) {\ttfamily SpotifyPlayer}
-- +(0, -3.5) node[right] {calls}
-- +(0, -4) node[sizedbox, fill=gray] (delegate) {\parbox{\textwidth}{\ttfamily SpotifyPlayer Delegate}};
\draw[link] (ui) edge[bend right=32] node[below, sloped] {\footnotesize actions} (model);
\draw[link] (model) edge[bend right=10] node[above, sloped] {\footnotesize events} (listeners);
\draw[link] (model) edge[bend left=10] node[above, sloped] {\footnotesize events} (notifier);
\draw[link] (delegate) edge[bend left=32] node[below, sloped] {\footnotesize actions} (model);
\draw[dashed, gray] ($(notifier.north west) + (-0.25, 0.25)$) rectangle ($(delegate.south east) + (0.25, -0.25)$);
\draw[dashed, gray] ($(listeners.north west) + (-0.25, 0.25)$) rectangle ($(ui.south east) + (0.25, -0.25)$);
\end{tikzpicture}
\caption{The player subsystem}
\label{fig:player}
\end{figure}
\subsection{Another listener: the MPRIS subsystem}
Similarly, the MPRIS subsystem follows that same pattern. It spawns a small DBUS server that translates DBUS messages to actions, and an \texttt{AppPlaybackStateListener} listens to incoming events.
One major difference is that the MPRIS server maintains its own state here, since the app state cannot be accessed from outside the main thread. To make sure this local state stays in sync, DBUS messages should not alter the local state directly -- instead, we should wait for a roundtrip through the app and incoming events.
\section{Components}
\subsection{Overview}
Components are thin wrappers around Gtk widgets, dedicated to binding them so that they produce the right actions, and updating them when specific events occur by conforming to \texttt{EventListener}.
\subsection{Modeling interactions}
Components should have some associated \texttt{struct} to model the interactions with the rest of the app. Let's consider the play/pause button as an example. Its behavior is defined in the \texttt{PlaybackModel}:
\begin{minted}{rust}
impl PlaybackModel {
fn is_playing(&self) -> bool { /**/ }
fn toggle_playback(&self) { /**/ }
}
\end{minted}
What we need to make our button work is a way to know its current state (is a song playing?) and a way to change that state (toggling on activation). Note that it would be tempting to simply query the widget's state, which \emph{should} be in sync with the actual playback state, but what we should really do instead is query the app state, which is the one source of truth for anything state-related.
Why do this? First, toggling the playback might fail (e.g. if no song is playing), but more importantly something else could alter the playback state (e.g. a DBUS query).
\begin{minted}{rust}
fn is_playing(&self) -> bool {
self.app_model.get_state().playback.is_playing()
}
\end{minted}
As for toggling the playback, remember that we can only mutate the state through actions (the \mintinline{rust}|get_state| call above returns some \mintinline{rust}|Deref<Target = AppState>|). In other words, we express what kind of action we want to perform, with no guarantee that it'll succeed.
\begin{minted}{rust}
fn toggle_playback(&self) {
self.dispatcher.dispatch(PlaybackAction::TogglePlay.into());
}
\end{minted}
\subsection{Binding the widget}
All that's left is binding the widget to our model. By wrapping our model in an \texttt{Rc}, it becomes easy to clone it into the kind of \texttt{'static} closure Gtk needs.
\begin{minted}{rust}
// model is an Rc<PlaybackModel>
widget.connect_play_pause(clone!(@weak model => move || model.toggle_playback()));
\end{minted}
Finally, we need our component to listen to relevant events, and update our widget accordingly.
\begin{minted}{rust}
impl EventListener for PlaybackControl {
fn on_event(&mut self, event: &AppEvent) {
match event {
AppEvent::PlaybackEvent(PlaybackEvent::PlaybackPaused)
| AppEvent::PlaybackEvent(PlaybackEvent::PlaybackResumed) => {
let is_playing = self.model.is_playing();
self.widget.set_playing(is_playing);
}
/**/
}
}
}
\end{minted}
\end{document}

3
doc/enter.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
docker build --network=host -t spot-doc .
docker run --rm -it -e THEUID="$(id -u "$USER")" -v "$PWD":/var/doxerlive spot-doc ash

112
flake.lock Normal file
View file

@ -0,0 +1,112 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1731098351,
"narHash": "sha256-HQkYvKvaLQqNa10KEFGgWHfMAbWBfFp+4cAgkut+NNE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "ef80ead953c1b28316cc3f8613904edc2eb90c28",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1731245184,
"narHash": "sha256-vmLS8+x+gHRv1yzj3n+GTAEObwmhxmkkukB2DwtJRdU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "aebe249544837ce42588aa4b2e7972222ba12e8f",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1728538411,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1731464916,
"narHash": "sha256-WZ5rpjr/wCt7yBOUsvDE2i22hYz9g8W921jlwVktRQ4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "2c19bad6e881b5a154cafb7f9106879b5b356d1f",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

89
flake.nix Normal file
View file

@ -0,0 +1,89 @@
{
description = "A GUI for yt-dlp written in Rust";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:nixos/nixpkgs?ref=nixpkgs-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
flake-utils,
crane,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (system:
let
craneLib = (crane.mkLib nixpkgs.legacyPackages.${system});
pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; };
nativeBuildInputs = with pkgs; [
gettext
meson
ninja
pkg-config
gtk4 # for gtk-update-icon-cache
glib # for glib-compile-schemas
desktop-file-utils
cargo
rustPlatform.cargoSetupHook
rustc
wrapGAppsHook4
blueprint-compiler
];
buildInputs = with pkgs; [
glib
gtk4
libadwaita
libhandy
openssl
alsa-lib
libpulseaudio
gst_all_1.gst-plugins-base
gst_all_1.gstreamer
];
cargoArtifacts = craneLib.buildDepsOnly ({
src = craneLib.cleanCargoSource (craneLib.path ./.);
inherit buildInputs nativeBuildInputs;
pname = "spot";
});
in with pkgs; {
packages = rec {
spot = craneLib.buildPackage {
src = craneLib.path ./.;
inherit buildInputs nativeBuildInputs cargoArtifacts;
};
default = spot;
};
devShells.default = mkShell {
inherit buildInputs nativeBuildInputs;
packages = with pkgs; [
(rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rust-analyzer" ];
})
meson
ninja
cargo
];
};
}) // {
overlays.default = final: prev: {
inherit (self.packages.${final.system}) spot;
};
};
}

25
meson.build Normal file
View file

@ -0,0 +1,25 @@
project(
'spot',
version: '0.5.0',
meson_version: '>= 0.59.0',
default_options: ['warning_level=2', 'buildtype=release'],
)
subdir('data')
subdir('po')
subdir('src')
flatpak_cargo_generator = find_program(meson.project_source_root() / 'build-aux/flatpak-cargo-generator.py')
cargo_sources = custom_target(
'cargo-update-sources',
build_by_default: false,
output: 'cargo-sources.json',
input: meson.project_source_root() / 'Cargo.lock',
command: [
flatpak_cargo_generator,
'@INPUT@',
'-o', '@SOURCE_ROOT@/cargo-sources.json'
]
)

2
meson_options.txt Normal file
View file

@ -0,0 +1,2 @@
option('offline', type: 'boolean', value: true)
option('features', type: 'string', value: '')

1
po/LINGUAS Normal file
View file

@ -0,0 +1 @@
en ru fr nl de ca cs pl es pt tr id pt-br fi eu ia nb sl ja ar it uk bn bg et

47
po/POTFILES Normal file
View file

@ -0,0 +1,47 @@
# grep gettext src/**/*.rs | cut -d: -f1 | uniq
src/app/batch_loader.rs
src/app/components/device_selector/widget.rs
src/app/components/labels.rs
src/app/components/login/login_model.rs
src/app/components/mod.rs
src/app/components/navigation/factory.rs
src/app/components/notification/mod.rs
src/app/components/playback/playback_controls.rs
src/app/components/playback/playback_info.rs
src/app/components/selection/component.rs
src/app/components/sidebar/sidebar_item.rs
src/app/components/sidebar/sidebar.rs
src/app/components/user_menu/user_menu.rs
src/app/state/login_state.rs
src/connect/player.rs
src/main.rs
# find src -name "*.blp" -print
src/window.blp
src/app/components/saved_playlists/saved_playlists.blp
src/app/components/artist_details/artist_details.blp
src/app/components/saved_tracks/saved_tracks.blp
src/app/components/search/search.blp
src/app/components/settings/settings.blp
src/app/components/artist/artist.blp
src/app/components/user_details/user_details.blp
src/app/components/selection/selection_toolbar.blp
src/app/components/scrolling_header/scrolling_header.blp
src/app/components/details/album_header.blp
src/app/components/details/release_details.blp
src/app/components/details/details.blp
src/app/components/now_playing/now_playing.blp
src/app/components/login/login.blp
src/app/components/playlist_details/playlist_details.blp
src/app/components/playlist_details/playlist_header.blp
src/app/components/playlist_details/playlist_headerbar.blp
src/app/components/headerbar/headerbar.blp
src/app/components/device_selector/device_selector.blp
src/app/components/sidebar/create_playlist.blp
src/app/components/sidebar/sidebar_row.blp
src/app/components/album/album.blp
src/app/components/playlist/song.blp
src/app/components/playback/playback_widget.blp
src/app/components/playback/playback_info.blp
src/app/components/playback/playback_controls.blp
src/app/components/library/library.blp

393
po/ar.po Normal file
View file

@ -0,0 +1,393 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural= n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "شاهد ألبوم"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "نسخ الرابط"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "أضف إلى الصف"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "أحدف من الصف"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "أضف إلى {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "لايمكن حفظ كلمة المرور. تحقق من أن ال Session Keyring مفتوح."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "حدث خطأ. تحقق من الlog لمزيد التفاصيل!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "مكتبة"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "قائمة أغاني"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "يشتغل الأن"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "لا توجد أغنية طور التشغيل"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "عن"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "غادر"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "تسجيل الخروج"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "إعادة الاتصال"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "أفضل الأغاني"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "أصدارات"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "قم بتسجيل الدخول إلى Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "أسم المستخدم"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "كلمة السر"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "فشل عملىة التحقق!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "تسجيل دخول"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "ألبومات"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "الفنانين"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "أغاني محفوظة"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "أوقف"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "شغل"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "عشوائي"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "سابق"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "التالي"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "كرر"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "ليس لديك قوائم تشغيل محفوظة."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "قائمات التشغيل خاصتك ستظهر هنا."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "ليس لديك ألبومات مسجلة."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "المكتبة خاصتك ستظهر هنا."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} تم تحديد صفر أغنية"
msgstr[1] "{} تم تحديد أغنية واحدة"
msgstr[2] "{} تم تحديد أغنيتان"
msgstr[3] "{} تم تحديد بعض الأغاني"
msgstr[4] "{} تم تحديد عديد الأغاني"
msgstr[5] "{} الأغاني الأخرى المحددة"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "أبحث في Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "أكتب للبجث."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "أحدف"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "أضف إلى قائمة التشغيل..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "ألغاء"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "اختر الكل"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "{} المزيد من"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "الناشر"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "أطلقت"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "الأغاني"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "المدة"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "حقوق النشر"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{}بواسطة {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

389
po/bg.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: bg\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr ""
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr ""
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr ""
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr ""
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr ""
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr ""
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr ""
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr ""
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr ""
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr ""
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr ""
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr ""
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr ""
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr ""
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr ""
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr ""
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr ""
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr ""
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr ""
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr ""
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr ""
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr ""
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr ""
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr ""
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr ""
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr ""
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr ""
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr ""
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] ""
msgstr[1] ""
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr ""
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr ""
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr ""
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr ""
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr ""
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr ""
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr ""
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr ""
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr ""
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr ""
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr ""
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

438
po/bn.po Normal file
View file

@ -0,0 +1,438 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: bn\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
#, fuzzy
msgid "View album"
msgstr "অ্যালবাম দেখুন"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
#, fuzzy
msgid "Copy link"
msgstr "লিংক কপি করুন"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
#, fuzzy
msgid "Add to queue"
msgstr "সারিতে যোগ করুন"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
#, fuzzy
msgid "Remove from queue"
msgstr "সারি থেকে সরান"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
#, fuzzy
msgid "Add to {}"
msgstr "{} তে যোগ করুন"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
#, fuzzy
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "পাসওয়ার্ড সেভ করা যায়নি। সেশন কীরিং আনলক করা আছে কিনা নিশ্চিত করুন।"
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
#, fuzzy
msgid "An error occured. Check logs for details!"
msgstr "একটি ত্রুটি ঘটেছে। বিস্তারিত জানতে লগ চেক করুন!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
#, fuzzy
msgid "Library"
msgstr "লাইব্রেরী"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
#, fuzzy
msgid "Playlists"
msgstr "প্লেলিস্ট"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
#, fuzzy
msgid "Now playing"
msgstr "এখন প্লে হচ্ছে"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
#, fuzzy
msgid "No song playing"
msgstr "কোনো গান প্লে হচ্ছেনা"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
#, fuzzy
msgid "About"
msgstr "অ্যাবাউট"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
#, fuzzy
msgid "Quit"
msgstr "প্রস্থান করুন"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
#, fuzzy
msgid "Log out"
msgstr "লগ আউট"
#: src/app/state/login_state.rs:115
#, fuzzy
msgid "Connection restored"
msgstr "সংযোগ পুনরুদ্ধার করা হয়েছে"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
#, fuzzy
msgid "Top tracks"
msgstr "টপ ট্র্যাকগুলি"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
#, fuzzy
msgid "Releases"
msgstr "রিলিজ"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
#, fuzzy
msgid "Login to Spotify Premium"
msgstr "স্পটিফাই প্রিমিয়ামে লগইন করুন"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
#, fuzzy
msgid "Username"
msgstr "ইউজারনেম"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
#, fuzzy
msgid "Password"
msgstr "পাসওয়ার্ড"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
#, fuzzy
msgid "Authentication failed!"
msgstr "অথেন্টিকেশন ব্যর্থ হয়েছে!"
#. Log in button label
#: src/app/components/login/login.blp:131
#, fuzzy
msgid "Log in"
msgstr "লগ ইন"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
#, fuzzy
msgid "Albums"
msgstr "অ্যালবাম"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
#, fuzzy
msgid "Artists"
msgstr "আর্টিস্ট"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
#, fuzzy
msgid "Saved tracks"
msgstr "সেভ করা ট্র্যাকগুলি"
#: src/app/components/playback/playback_controls.rs:64
#, fuzzy
msgid "Pause"
msgstr "পজ"
#: src/app/components/playback/playback_controls.rs:66
#, fuzzy
msgid "Play"
msgstr "প্লে"
#: src/app/components/playback/playback_controls.blp:15
#, fuzzy
msgid "Shuffle"
msgstr "শাফল"
#: src/app/components/playback/playback_controls.blp:24
#, fuzzy
msgid "Previous"
msgstr "পূর্ববর্তী"
#: src/app/components/playback/playback_controls.blp:46
#, fuzzy
msgid "Next"
msgstr "পরবর্তী"
#: src/app/components/playback/playback_controls.blp:55
#, fuzzy
msgid "Repeat"
msgstr "রিপিট"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
#, fuzzy
msgid "You have no saved playlists."
msgstr "আপনার কোনো সেভ করা প্লেলিস্ট নেই।"
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
#, fuzzy
msgid "Your playlists will be shown here."
msgstr "আপনার প্লেলিস্ট এখানে দেখানো হবে।"
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
#, fuzzy
msgid "You have no saved albums."
msgstr "আপনার কোনো সেভ করা অ্যালবাম নেই।"
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
#, fuzzy
msgid "Your library will be shown here."
msgstr "আপনার লাইব্রেরী এখানে দেখানো হবে।"
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
#, fuzzy
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} টি গান সিলেক্ট করা হয়েছে"
msgstr[1] "{} টি গান সিলেক্ট করা হয়েছে"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
#, fuzzy
msgid "Search Spotify."
msgstr "স্পটিফাইয়ে খুঁজুন।"
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
#, fuzzy
msgid "Type to search."
msgstr "খোঁজার জন্য এখানে টাইপ করুন।"
#: src/app/components/selection/selection_toolbar.blp:61
#, fuzzy
msgid "Remove"
msgstr "রিমুভ করুন"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
#, fuzzy
msgid "Add to playlist..."
msgstr "প্লেলিস্টে যোগ করুন..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
#, fuzzy
msgid "Cancel"
msgstr "বাতিল করুন"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
#, fuzzy
msgid "Select all"
msgstr "সবকিছু সিলেক্ট করুন"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
#, fuzzy
msgid "More from {}"
msgstr "{} -র থেকে আরও"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
#, fuzzy
msgid "Label"
msgstr "লেবেল"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
#, fuzzy
msgid "Released"
msgstr "রিলিজ হয়েছে"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
#, fuzzy
msgid "Tracks"
msgstr "ট্র্যাক গুলি"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
#, fuzzy
msgid "Duration"
msgstr "সময়"
#: src/app/components/details/release_details.blp:72
#, fuzzy
msgid "Copyright"
msgstr "কপিরাইট"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
#, fuzzy
msgid "{} by {}"
msgstr "{} বাই {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

391
po/ca.po Normal file
View file

@ -0,0 +1,391 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: ca\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Mostra l'àlbum"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Copia l'enllaç"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.ui:39
msgid "Add to queue"
msgstr "Afegeix a la cua"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Suprimeix de la cua"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Afegeix a {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:56
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "No s'ha pogut desar la sessió. Assegura't que l'anell de claus de la sessió està desbloquejat."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:128
msgid "An error occured. Check logs for details!"
msgstr "Ha ocorregut un error. Comprova els registres per més detalls!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Biblioteca"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Llistes de reproducció"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "S'està reproduïnt"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.ui:32
msgid "No song playing"
msgstr "Cap cançó en reproducció"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Quant a"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Surt"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Tanca la sessió"
#: src/app/state/login_state.rs:112
msgid "Connection restored"
msgstr "Connexió restablerta"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.ui:26
msgid "Top tracks"
msgstr "Millors pistes"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.ui:53
msgid "Releases"
msgstr "Llançaments"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.ui:45
msgid "Login to Spotify Premium"
msgstr "Inicia la sessió a Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.ui:72
msgid "Username"
msgstr "Nom d'usuari"
#. Placeholder for the password field
#: src/app/components/login/login.ui:89
msgid "Password"
msgstr "Contrasenya"
#. This error is shown when authentication fails.
#: src/app/components/login/login.ui:114
msgid "Authentication failed!"
msgstr "L'autenticació ha fallat!"
#. Log in button label
#: src/app/components/login/login.ui:129
msgid "Log in"
msgstr "Inicia sessió"
#. This is the title of a section of the search results
#: src/app/components/search/search.ui:72
msgid "Albums"
msgstr "Àlbums"
#. This is the title of a section of the search results
#: src/app/components/search/search.ui:105
msgid "Artists"
msgstr "Artistes"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Pistes desades"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pausa"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Reprodueix"
#: src/app/components/playback/playback_controls.ui:17
msgid "Shuffle"
msgstr "Barreja"
#: src/app/components/playback/playback_controls.ui:27
msgid "Previous"
msgstr "Anterior"
#: src/app/components/playback/playback_controls.ui:50
msgid "Next"
msgstr "Següent"
#: src/app/components/playback/playback_controls.ui:60
msgid "Repeat"
msgstr "Repeteix"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.ui:26
msgid "You have no saved playlists."
msgstr "No teniu llistes de reproducció desades."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.ui:27
msgid "Your playlists will be shown here."
msgstr "Les vostres llistes de reproducció es mostraran aquí."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.ui:26
msgid "You have no saved albums."
msgstr "No teniu àlbums desats."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.ui:27
msgid "Your library will be shown here."
msgstr "Aquí es mostrarà la vostra biblioteca."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} cançó seleccionada"
msgstr[1] "{} cançons seleccionades"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.ui:116
msgid "Search Spotify."
msgstr "Cerca a Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.ui:117
msgid "Type to search."
msgstr "Escriu per començar a buscar."
#: src/app/components/selection/selection_toolbar.ui:69
msgid "Remove"
msgstr "Elimina"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.ui:56
msgid "Add to playlist..."
msgstr "Afegeix a la llista..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.ui:50
#: src/app/components/headerbar/headerbar.ui:47
msgid "Cancel"
msgstr "Cancel·la"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.ui:58
msgid "Select all"
msgstr "Selecciona-ho tot"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Més de {}"
#. This refers to a music label
#: src/app/components/details/release_details.ui:38
msgid "Label"
msgstr "Etiqueta"
#. This refers to a release date
#: src/app/components/details/release_details.ui:48
msgid "Released"
msgstr "Llançaments"
#. This refers to a number of tracks
#: src/app/components/details/release_details.ui:58
msgid "Tracks"
msgstr "Millors pistes"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Duració"
#: src/app/components/details/release_details.ui:68
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} per {}"
#: src/app/components/sidebar/sidebar.rs:48
msgid "Unnamed playlist"
msgstr "Llista sense nom"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Totes les llistes"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Cançons desades!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Preferències"
#: src/main.rs:80
msgid "Failed to open link!"
msgstr "No s'ha pogut obrir l'enllaç!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.ui:13
msgid "Audio"
msgstr "Àudio"
#. Title for an item in preferences
#: src/app/components/settings/settings.ui:16
msgid "Audio Backend"
msgstr "Sistema d'àudio"
#. Title for an item in preferences
#: src/app/components/settings/settings.ui:29
msgid "ALSA Device"
msgstr "Dispositiu ALSA"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.ui:30
msgid "Applied only if audio backend is ALSA"
msgstr "Usat només si el sistema d'àudio és l'ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.ui:40
msgid "Audio Quality"
msgstr "Qualitat de l'àudio"
#: src/app/components/settings/settings.ui:44
msgid "Normal"
msgstr "Normal"
#. Qualitat d'àudio -> femení, la qualitat
#: src/app/components/settings/settings.ui:45
msgid "High"
msgstr "Alta"
#. El mateix que amb "High". Cal usar el femení
#: src/app/components/settings/settings.ui:46
msgid "Very high"
msgstr "Molt alta"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.ui:70
msgid "Appearance"
msgstr "Aparença"
#. Title for an item in preferences
#: src/app/components/settings/settings.ui:73
msgid "Theme"
msgstr "Tema"
#: src/app/components/settings/settings.ui:77
msgid "Light"
msgstr "Clar"
#: src/app/components/settings/settings.ui:78
msgid "Dark"
msgstr "Fosc"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.ui:89
msgid "Network"
msgstr "Xarxa"
#. Title for an item in preferences
#: src/app/components/settings/settings.ui:92
msgid "Access Point Port"
msgstr "Port del punt d'accés"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.ui:93
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "El port usat per les connexions al punt d'accés de l'Spotify. Establiu-ho a 0 si qualsevol port és correcte."
#: src/app/components/selection/selection_toolbar.ui:90
msgid "Save to library"
msgstr "Desa-ho a la llibreria"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.ui:54
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.ui:79
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.ui:63
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.ui:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.ui:33
msgid "Create"
msgstr ""

393
po/cs.po Normal file
View file

@ -0,0 +1,393 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: cs\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Zobrazit album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Zkopírovat odkaz"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Přidat do fronty"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Odebrat z fronty"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Přidat do {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Heslo nelze uložit. Zkontrolujte, že je klíčenka (session keyring) odemčená."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Došlo k chybě. Podrobnosti najdete v protokolu."
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Knihovna"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Playlisty"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Právě hraje"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Nic nehraje"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "O aplikaci"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Konec"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Odhlásit se"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Připojení obnoveno"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Populární"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Diskografie"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Přihlásit se ke Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Uživatelské jméno"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Heslo"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Chyba autentizace!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Přihlásit se"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Alba"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Umělci"
#. Where is this text displayed?
#. I see it is supposed to be in `src/app/components/navigation/home.rs:47`, but that does not seem to be the case: https://github.com/xou816/spot/blob/1b96cd07129f27c7f3f3a2e66299c44a52e489d4/src/app/components/navigation/home.rs
#. Maybe it is something that has not been commited yet (2021-10-02T10:42:52+02:00), but @xou816 has it locally and has generated pot / po files from it in https://github.com/xou816/spot/commit/86414c03d207cdf4e2c1cbddd47c7ca5629d1303
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Uložené skladby"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pozastavit"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Přehrát"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Náhodné přehrávání"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Předchozí"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Následující"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Opakovat"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Nemáte žádné uložené playlisty."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Tady budou zobrazeny vaše playlisty."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Nemáte žádná uložená alba."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Tady bude zobrazena vaše knihovna."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} skladba vybrána"
msgstr[1] "{} skladby vybrány"
msgstr[2] "{} skladeb vybráno"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Hledat na Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Zadejte a vyhledejte."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Odstranit"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Přidat do playlistu..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Zrušit"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Vybrat vše"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Více od {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Vydavatelství"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Vydáno"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Počet skladeb"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Délka"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} od {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Playlist bez názvu"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Všechny playlisty"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Skladby uloženy!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Nastavení"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Nepodařilo se otevřít odkaz!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Zvuk"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Zvukový backend"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "ALSA zařízení"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Použito pouze pokud zvukový backend je ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Kvalita zvuku"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normální"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Vysoká"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Velmi vysoká"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Vzhled"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Téma"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Světlé"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Tmavé"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Síť"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Port Access Pointu"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Port používaný pro připojení ke Spotify Access Point. Nastavte na 0 pokud lze použít libovolný port."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Uložit do knihovny"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Playlist byl vytvořen."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Zobrazit"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Přidat playlist"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Přehrávání bez mezer"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Systém"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Hotovo"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Název"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Vytvořit"

389
po/de.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Album ansehen"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Link kopieren"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Zur Warteschlange hinzufügen"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Von Warteschlange entfernen"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Zu {} hinzufügen"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Das Passwort konnte nicht gespeichert werden. Stellen Sie sicher, dass der Sitzungs-Schlüsselbund entsperrt ist."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Es ist ein Fehler aufgetreten. Überprüfen Sie die Protokolle für Details!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Bibliothek"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Playlists"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Wiedergabe"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Keine Wiedergabe"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Info"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Beenden"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Abmelden"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Verbindung wiederhergestellt"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Beliebte Titel"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Diskografie"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Bei Spotify Premium anmelden"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Benutzername"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Passwort"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Anmeldung fehlgeschlagen!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Anmelden"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Alben"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Künstler"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Gespeicherte Titel"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pause"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Play"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Shuffle"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Zurück"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Weiter"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Wiederholen"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Keine gespeicherten Playlists vorhanden."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Playlists werden hier angezeigt."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Keine gespeicherten Alben vorhanden."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Die Bibliothek wird hier angezeigt."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} Titel ausgewählt"
msgstr[1] "{} Titel ausgewählt"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Auf Spotify suchen."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Tippe um zu suchen."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Entfernen"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Zur Playlist hinzufügen..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Abbrechen"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Alle auswählen"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Mehr von {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Label"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Veröffentlicht"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Titel"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Dauer"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} von {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Unbenannte Playlist"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Alle Playlists"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Titel gespeichert!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Einstellungen"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Öffnen des Links fehlgeschlagen!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Audio Backend"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "ALSA Gerät"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Wird nur angewendet, wenn das Audio-Backend ALSA ist"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Audioqualität"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normal"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Hoch"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Sehr hoch"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Aussehen"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Thema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Hell"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Dunkel"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Netzwerk"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Access Point Port"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Port, der für Verbindungen mit dem Spotify Access Point verwendet wird. Setze den Wert auf 0 für einen beliebigen Port."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "In Bibliothek speichern"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Neue Playlist erstellt."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Anzeigen"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Neue Playlist"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Lückenlose Wiedergabe"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "System"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Fertig"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Name"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Erstellen"

389
po/en.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "View album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Copy link"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Add to queue"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Remove from queue"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Add to {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Could not save password. Make sure the session keyring is unlocked."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "An error occured. Check logs for details!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Library"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Playlists"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Now playing"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "No song playing"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "About"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Quit"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Log out"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Connection restored"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Top tracks"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Releases"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Login to Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Username"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Password"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Authentication failed!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Log in"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albums"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artists"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Saved Tracks"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pause"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Play"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Shuffle"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Previous"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Next"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Repeat"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "You have no saved playlists."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Your playlists will be shown here."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "You have no saved albums."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Your library will be shown here."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} song selected"
msgstr[1] "{} songs selected"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Search Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Type to search."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Remove"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Add to playlist..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Cancel"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Select all"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "More from {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Label"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Released"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Tracks"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Duration"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} by {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Unnamed playlist"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

389
po/es.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Ver álbum"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Copiar enlace"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Añadir a la cola"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Borrar de la lista"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Añadir a {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "No se pudo guardar la contraseña. Asegúrate de que el anillo de claves esta desbloqueado."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Ha ocurrido un error. ¡Revisa los logs para más detalles!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Biblioteca"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Lista de reproducción"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Reproduciendo ahora"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Ninguna canción en reproducción"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Acerca de"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Salir"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Cerrar sesión"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Conexión restaurada"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Mejores canciones"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Lanzamientos"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Iniciar sesión en Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Usuario"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Contraseña"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "¡Autenticación fallida!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Iniciar sesión"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Álbumes"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artistas"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Pistas guardadas"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pausa"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Reproducir"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Activar aleatorio"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Anterior"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Siguiente"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Activar repetir"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "No tienes listas de reproducción guardadas."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Tus listas de reproducción se mostrarán aquí."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "No tienes álbumes guardados."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Tu biblioteca se mostrará aquí."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} canción seleccionada"
msgstr[1] "{} canciones seleccionadas"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Buscar en Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Escribe para buscar."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Retirar"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Añadir a la lista de reproducción..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Cancelar"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Seleccionar todo"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Más de {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Etiqueta"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Lanzado"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Canciones"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Duración"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} de {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Lista de reproducción sin nombre"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Todas las listas de reproducción"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "¡Pistas guardadas!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Preferencias"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "¡No se ha podido abrir el enlace!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Sonido"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Backend de audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "Dispositivo ALSA"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Sólo se aplica si el backend de audio es ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Calidad de sonido"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normal"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Alta"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Muy alta"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Apariencia"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Tema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Claro"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Oscuro"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Red"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Puerto del punto de acceso"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Puerto utilizado para las conexiones con el punto de acceso de Spotify. Poner a 0 si cualquier puerto está bien."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Guardar en la biblioteca"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

389
po/et.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: et\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Vaata albumit"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Kopeeri link"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Lisa järjekorda"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Eemalda järjekorrast"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Lisa {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Salasõna salvestamine ebaõnnestus. Kindel, et sessiooni võtmering on lukust lahti?"
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Tekkis viga. Rohkem infot logides."
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Teek"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Esitusloendid"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Praegu mängib"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Ükski laul ei mängi"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Teave"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Sulge"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Logi välja"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Ühendus taastatud"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Populaarsemad lood"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Diskograafia"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Logi sisse Spotify Premiumisse"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Kasutajanimi"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Salasõna"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Autentimine ebaõnnestus!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Logi sisse"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albumid"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Esitajad"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Salvestatud lood"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Peata"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Esita"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Juhuesitus"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Eelmine"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Järgmine"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Korda"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Sul pole ühtegi salvestatud esitusloendit."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Sinu esitusloendid näidatakse siin."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Sul pole ühtegi salvestatud albumit."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Sinu teeki näidatakse siin."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} lugu valitud"
msgstr[1] "{} lugu valitud"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Otsi Spotify'st."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Tipi, et otsida."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Eemalda"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Lisa esitusloendisse..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Loobu"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Vali kõik"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Rohkem esitajast {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Väljaandja"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Välja antud"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Lood"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Pikkus"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Autoriõigused"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} - {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Nimetamata esitusloend"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Kõik esitusloendid"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Lugu salvestatud!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Eelistused"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Lingi avamine ebaõnnestus!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Heli"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Heli sisend"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "ALSA heliseade"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Kehtib ainult, kui heli sisend on ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Heli kvaliteet"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Keskmine"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Kõrge"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Väga kõrge"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Välimus"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Teema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Hele teema"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Tume teema"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Võrk"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Sissepääsu port"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Port Spotify ühendamiseks. Säti 0, et lubada iga port."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Salvesta teeki"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

389
po/eu.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: eu\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Albuma ikusi"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Kopiatu esteka"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Gehitu isatsari"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Ezabatu isatsetik"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Gehitu {} (r)i"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Ezin izan da pasahitza gorde. Ziurtatu saioko giltzatakoa desblokeatuta dagoela."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Errore bat gertatu da. Egiaztatu xehetasunak erregistroetan!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Biblioteka"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Erreprodukzio-zerrendak"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Orain erreproduzitzen"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Ez da abestirik erreproduzitzen ari"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Honi buruz"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Utzi"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Saioa itxi"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Konexioa leheneratu da"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Pista nagusiak"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Jaurtiketak"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Saioa hasi Spotify Premiumen"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Erabiltzailearen izena"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Pasahitza"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Autentifikazioak huts egin du!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Hasi saioa"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albumak"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artistak"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Gordetako pistak"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Gelditu"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Erreproduzitu"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Nahasketa"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Aurrekoa"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Hurrengoa"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Errepikatu"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Ez duzu erreprodukzio zerrendarik gordeta."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Zure erreprodukzio zerrendak hemen agertuko dira."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Ez duzu albumik gordeta."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Zure liburutegia hemen agertuko da."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} Aukeratutako abesti bat"
msgstr[1] "{} Aukeratutako abestiak"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Bilatu Spotifyn."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Idatzi bilatzeko."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Ezabatu"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Erreprodukzio-zerrendara gehitu..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Ezeztatu"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Hautatu dena"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Gehiago honi buruz {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Etiketa"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Argitaratua"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Pistak"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Iraupena"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Egile-eskubideak"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} {} -ek eginda"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

389
po/fi.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Näytä albumi"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Kopioi linkki"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Lisää jonoon"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Poista jonosta"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Lisää kohteeseen {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Salasanaa ei voitu tallentaa. Varmista, että avainnippua ei ole lukittu."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Tapahtui virhe. Katso lisätiedot lokista!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Kirjasto"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Soittolistat"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Nyt toistetaan"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Ei toistettavaa kappaletta"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Tietoja"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Lopeta"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Kirjaudu ulos"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Yhteys palautunut"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Suosituimmat kappaleet"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Julkaisut"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Kirjaudu Spotify Premiumiin"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Käyttäjätunnus"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Salasana"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Tunnistautuminen epäonnistui!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Kirjaudu sisään"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albumit"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Esittäjät"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Tallennetut kappaleet"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Keskeytä"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Toista"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Sekoita"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Edellinen"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Seuraava"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Kertaa"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Ei tallennettuja soittolistoja."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Soittolistasi näytetään täällä."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Ei tallennettuja albumeja."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Kirjastosi näytetään täällä."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} kappale valittu"
msgstr[1] "{} kappaletta valittu"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Etsi Spotifysta."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Kirjoita etsiäksesi."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Poista"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Lisää soittolistaan..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Peru"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Valitse kaikki"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Lisää esittäjältä {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Levy-yhtiö"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Julkaistu"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Kappaleita"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Kesto"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Tekijänoikeus"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} tehnyt {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Nimetön soittolista"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Kaikki soittolistat"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Kappaleet tallennettu!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Asetukset"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Linkin avaaminen epäonnistui!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Ääni"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Äänen taustaosa"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "ALSA-laite"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Sovelletaan vain jos äänen taustaosa on ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Äänenlaatu"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normaali"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Korkea"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Hyvin korkea"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Ulkoasu"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Teema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Vaalea"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Tumma"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Verkko"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Access Pointin portti"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Spotifyn Access Pointiin tuleviin yhteyksiin käytettävä portti. Aseta arvoksi 0, jos mikä tahansa portti kelpaa."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Tallenna kirjastoon"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Uusi soittolista luotu."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Näytä"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Uusi soittolista"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Keskeytyksetön toisto"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Järjestelmä"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Valmis"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Nimi"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Luo"

389
po/fr.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Voir l'album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Copier le lien"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Ajouter à la file d'attente"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Retirer de la file d'attente"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Ajouter à {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Le mot de passe n'a pu être enregistré, assurez-vous que le Trousseau de session est déverouillé."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Une erreur est survenue. Consultez les journaux de débogage pour plus d'information."
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Bibliothèque"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Listes de lecture"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "En cours de lecture"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Aucune lecture en cours"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "À propos"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Quitter"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Déconnexion"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Connexion rétablie"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Morceaux populaires"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Discographie"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Connexion à Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Nom d'utilisateur"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Mot de passe"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "L'authentification a échoué !"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Connexion"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albums"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artistes"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Titres aimés"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pause"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Lecture"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Aléatoire"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Précédent"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Suivant"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Répéter"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Aucune liste de lecture enregistrée."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Vos listes de lectures apparaîtrons ici."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Aucun album dans votre bibliothèque."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Vos albums apparaîtrons ici."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} morceau sélectionné"
msgstr[1] "{} morceaux sélectionnés"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Rechercher dans Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Saisissez un terme à rechercher."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Retirer"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Ajouter à..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Annuler"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Tout sélectionner"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Plus de {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Label"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Date de publication"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Morceaux"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Durée"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} de {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Liste de lecture sans titre"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Toutes les listes de lecture"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Pistes enregistrées !"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Préférences"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Échec de l'ouverture du lien."
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Gestionnaire de l'audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "Appareil ALSA"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Ne s'applique que si ALSA est sélectionné"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Qualité du flux audio"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normale"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Élevée"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Très élevée"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Apparence"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Thème"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Clair"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Sombre"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Réseau"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Port du point d'accès"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Port utilisé pour se connecter au point d'accès de Spotify. Laisser la valeur à 0 pour gérer ce paramètre automatiquement. "
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Enregistrer dans la bibliothèque"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Liste de lecture créée."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Voir"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Nouvelle liste de lecture"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Lecture continue"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Valeur système"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Terminé"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Nom"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Créer"

389
po/ia.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: ia\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Vider album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Copiar ligamine"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Adder al cauda"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Remover ab cauda"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Adder a {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Non poteva salvar le contrasigno. Assecura te qui le magazin de claves es disblocate."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Ocurreva un error. Verifica le registros pro detalios!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Bibliotheca"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Listas de reproduction"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Ora in reproduction"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Nulle canto in reproduction"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "A proposito de"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Quitar"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Clauder le session"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Connexion restabilite"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Melior tracias"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Lanciamentos"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Initiar session in Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Nomine de usator"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Contrasigno"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Falleva le authentication!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Initiar session"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albumes"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artistas"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Tracias salvate"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pausar"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Reproducer"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Aleatori"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Previe"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Sequente"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Repeter"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Tu non ha listas de reproduction salvate."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Tu listas de reproduction essera monstrate hic."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Tu ha nulle albumes salvate."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Tu bibliotheca essera monstrate ci."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} canto seligite"
msgstr[1] "{} cantos seligite"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Cercar Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Scribe pro cercar."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Remover"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Adder al lista de reproduction..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Cancellar"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Seliger toto"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Plus ab {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Sigillo"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Lanciate"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Tracias"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Duration"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} per {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Lista de reproduction sin nomine"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Tote le listas de reproduction"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Tracias salvate!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Preferentias"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Falleva in aperir ligamine!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Apparentia"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Thema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Clar"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Obscur"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Rete"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

388
po/id.po Normal file
View file

@ -0,0 +1,388 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: id\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Lihat album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Salin tautan"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Tambah ke Antrian"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Hapus dari antrian"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Tambah ke {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Tak bisa menyimpan sandi. Pastikan bahwa kunci sesi tidak terkunci."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Timbul galat. Periksa log untuk detailnya!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Pustaka"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Daftar putar"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Sedang memutar"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Tidak ada lagu yang diputar"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Tentang"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Keluar"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Log keluar"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Sambungan dipulihkan"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Trek teratas"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Rilis"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Masuk ke Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Nama Pengguna"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Kata Sandi"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Autentikasi gagal!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Log masuk"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Album"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artis"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Trek yang disimpan"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Jeda"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Putar"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Acak"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Sebelumnya"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Selanjutnya"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Ulangi"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Anda tidak memiliki daftar putar yang disimpan."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Daftar putar Anda akan ditampilkan di sini."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Anda tidak memiliki album yang disimpan."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Pustaka Anda akan ditampilkan di sini."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} lagu dipilih"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Cari Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Ketik untuk mencari."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Buang"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Tambah ke daftar putar…"
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Batal"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Pilih semua"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Lebih banyak dari {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Label"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Dirilis"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Trek"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Durasi"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Hak Cipta"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} oleh {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Daftar putar tanpa nama"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Semua Daftar Putar"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Trek disimpan!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Preferensi"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Gagal membuka tautan!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Backend Audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "Perangkat ALSA"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Diterapkan hanya jika backend audio adalah ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Kualitas Audio"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normal"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Tinggi"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Sangat tinggi"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Penampilan"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Tema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Terang"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Gelap"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Jaringan"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Port Titik Akses"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Port yang digunakan untuk koneksi ke Titik Akses Spotify. Atur ke 0 jika ada port yang baik-baik saja."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Simpan ke pustaka"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Daftar putar baru dibuat."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Tilik"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Daftar Putar Baru"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Pemutaran tanpa celah"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Sistem"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Selesai"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Nama"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Mencipta"

390
po/it.po Normal file
View file

@ -0,0 +1,390 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: it\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Guarda l'album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Copia link"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Aggiungi alla coda di riproduzione"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Rimuovi dalla coda di riproduzione"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Aggiungi a {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Impossibile salvare la password. Controlla che la session keyring sia sbloccata."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "C'è stato un errore. Controlla i log per i dettagli!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Raccolta"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Playlist"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "In riproduzione"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Nessun brano in riproduzione"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Informazioni su"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Esci"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Log out"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Connessione ripristinata"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Brani più popolari"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Releases"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Login su Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Username"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Password"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Autenticazione fallita!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Log in"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Album"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artisti"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Tracce salvate"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pausa"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Riproduci"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Mescola"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Precedente"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Successivo"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Ripeti"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Non hai playlist salvate."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Le tue playlist verranno mostrate qui."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Non hai album salvati."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "La tua libreria verrà mostrata qui."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} brano selezionato"
msgstr[1] "{} brani selezionati"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Cerca Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Digita per cercare."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Rimuovi"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Aggiungi alla playlist..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Annulla"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Seleziona tutto"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Di più da {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Etichetta discografica"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Data di uscita"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Tracce"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Durata"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} di {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Playlist senza nome"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Tutte le playlist"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Tracce salvate!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Preferenze"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Apertura del link fallita!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Backend Audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "Device ALSA"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Applicato solo se il backend audio è ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Qualità Audio"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normale"
#: src/app/components/settings/settings.blp:48
#, fuzzy
msgid "High"
msgstr "Alta"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Molto alta"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Aspetto"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Tema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Chiaro"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Scuro"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Rete"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Porta Access Point"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Porta usata per le connessioni all'Access Point Spotify. Impostala a 0 se qualsiasi valore va bene."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Salva nella libreria"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Creata nuova playlist."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Visualizza"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Nuova Playlist"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Riproduzione gapless"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Sistema"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Fatto"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Nome"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Crea"

389
po/ja.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: ja\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "アルバムを見る"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "リンクをコピー"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "キューに追加"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "キューから削除"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "{}に追加"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "パスワードを保存できませんでした。セッションキーリングのロックを解除してください。"
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "エラーが発生しました。詳細はログを確認してください。"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "ライブラリ"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "プレイリスト"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "再生中"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "再生中の曲はありません"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "このアプリについて"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "終了"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "ログアウト"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "再接続しました"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "人気のトラック"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "作品"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Spotifyプレミアムにログイン"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "ユーザー名"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "パスワード"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "認証に失敗しました。"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "ログイン"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "アルバム"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "アーティスト"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "お気に入り"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "一時停止"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "再生"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "ランダム"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "前"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "次"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "繰り返し"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "保存したプレイリストはありません。"
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "プレイリストがここに表示されます。"
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "保存したアルバムがありません。"
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "ライブラリはここに表示されます。"
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{}曲選択中"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "検索する。"
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "検索ワードを入力してください。"
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "削除"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "プレイリストに追加…"
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "選択解除"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "すべて選択"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
#, fuzzy
msgid "More from {}"
msgstr ""
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "レーベル"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "リリース"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "曲数"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "再生時間"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "著作権表示"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} から {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

2
po/meson.build Normal file
View file

@ -0,0 +1,2 @@
i18n = import('i18n')
i18n.gettext('spot', args: ['--from-code=UTF-8', '--add-comments'], preset: 'glib')

390
po/nb.po Normal file
View file

@ -0,0 +1,390 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: nb\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Vis album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Kopier lenke"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Legg til i køen"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Fjern fra køen"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Legg i {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Kunne ikke lagre passordet. Sørg for at nøkkelringen til sesjonen er låst opp."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "En feil oppstod. Sjekk logger for detaljer!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Bibliotek"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Spillelister"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Nå spiller"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Ingen sang spilles"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Om"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Avslutt"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Logg ut"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Tilkoblingen er gjenopprettet"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Topp spor"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Utgivelser"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Logg på Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Brukernavn"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Passord"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Autentiseringen mislyktes!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Logg inn"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Album"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artister"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Lagrede spor"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pause"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Spill av"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Tilfeldig"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Forrige"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Neste"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Gjenta"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Du har ingen lagrede spillelister."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Spillelistene dine vises her."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Du har ingen lagrede album."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Biblioteket ditt vises her."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
#, fuzzy
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} sang valgt"
msgstr[1] "{} sanger valgt"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Søk Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Skriv for å søke."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Fjern"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Legg til spilleliste..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Avbryt"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Velg alle"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Mer fra {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Utgiver"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Utgitt"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Spor"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Varighet"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Opphavsrett"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} av {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Navnløs spilleliste"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Alle Spillelister"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Spor lagret!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Innstillinger"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Kunne ikke åpne linken!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Lyd"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Lyd Backend"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "ALSA Enhet"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Brukes bare hvis lyd backend er ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Lyd Kvalitet"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normal"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Høy"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Veldig høy"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Utseende"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Tema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Lys"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Mørk"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Nettverk"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Aksesspunkt Port"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Port som brukes for tilkoblinger til Spotify sitt aksesspunkt. Sett til 0 hvis tilfeldig port er greit."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Lagre til bibliotek"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

389
po/nl.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: nl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Album bekijken"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Link kopiëren"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Toevoegen aan wachtrij"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Verwijderen uit wachtrij"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Toevoegen aan {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Het wachtwoord kan niet worden opgeslagen - zorg dat de sessiesleutelhanger ontgrendeld is."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Er is een fout opgetreden - bekijk het logboek!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Verzameling"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Afspeellijsten"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Je luistert naar"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Er wordt niks afgespeeld"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Over"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Afsluiten"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Uitloggen"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "De verbinding is hersteld"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Topnummers"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Uitgaven"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Inloggen op Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Gebruikersnaam"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Wachtwoord"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Het inloggen is mislukt."
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Inloggen"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albums"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artiesten"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Opgeslagen nummers"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pauzeren"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Afspelen"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Willekeurig"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Vorige"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Volgende"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Herhalen"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Je hebt nog geen opgeslagen afspeellijsten."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Je afspeellijsten worden hier getoond."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Je hebt nog geen opgeslagen albums."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Je verzameling wordt hier getoond."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} nummer geselecteerd"
msgstr[1] "{} nummers geselecteerd"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Doorzoek Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Typ om te zoeken"
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Verwijderen"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Toevoegen aan afspeellijst…"
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Annuleren"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Alles selecteren"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Meer van {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Label"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Uitgebracht"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Nummers"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Duur"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} van {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Naamloze afspeellijst"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Alle afspeellijsten"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "De nummers zijn opgeslagen!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Voorkeuren"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "De link kan niet worden geopend!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Audio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Audiobackend"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "ALSA-apparaat"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Alleen van toepassing als het audiobackend is ingesteld op ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Audiokwaliteit"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normaal"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Hoog"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Hoogst"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Vormgeving"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Thema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Licht"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Donker"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Netwerk"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Poort van toegangspunt"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "De poort die wordt gebruikt om verbinding te maken met Spotify's toegangspunt. 0 = elke poort."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Opslaan in verzameling"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "De afspeellijst is aangemaakt."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Bekijken"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Nieuwe afspeellijst"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Afspelen zonder pauzes"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Systeem"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Klaar"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Naam"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Aanmaken"

391
po/pl.po Normal file
View file

@ -0,0 +1,391 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: pl\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Zobacz album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Skopiuj link"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Dodaj do kolejki"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Usuń z kolejki"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Dodaj do {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Nie można zapisać hasła. Upewnij się że narzędzie zarządzające kluczami jest odblokowane."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Wystąpił błąd. Sprawdź logi po więcej informacji!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Biblioteka"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Playlisty"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Aktualnie odtwarzane"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Aktualnie nie gra żadna piosenka"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "O programie"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Wyjdź"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Wyloguj się"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Odzyskano połączenie"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Najlepsze piosenki"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Premiery"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Zaloguj się do Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Nazwa użytkownika"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Hasło"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Błąd autoryzacji!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Zaloguj"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albumy"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artyści"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Zachowane piosenki"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr ""
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr ""
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr ""
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr ""
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr ""
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr ""
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Nie masz zapisanych playlist."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Twoje playlisty będą tu pokazane."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Nie masz zapisanych albumów."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Twoja biblioteka będzie tu pokazana."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} wybrana piosenka"
msgstr[1] "{} wybrane piosenki"
msgstr[2] "{} wybranych piosenek"
msgstr[3] ""
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr ""
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr ""
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr ""
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr ""
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Więcej od {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Wydawca"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Premiery"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Najlepsze piosenki"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Długość"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Prawa autorskie"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr ""
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

5
po/poeditor.yml Normal file
View file

@ -0,0 +1,5 @@
api_token: 7ac85acabcb7842c23f5d4060485f25e # read only token
projects:
- format: po
id: 469205
terms_path: '{language_code}.po'

389
po/pt-br.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: pt-br\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Ver álbum"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Copiar link"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Adicionar a fila"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Remover da fila"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Adicionar a {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Não foi possível salvar a senha. Verifique se o seu chaveiro de sessão está desbloqueado."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Ocorreu um erro. Verifique os logs para mais detalhes!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Biblioteca"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Listas de reprodução"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Tocando agora"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Nenhum música sendo executada"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Sobre"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Fechar"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Sair"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Conexão reestabelecida"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Melhores faixas"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Lançamentos"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Entrar no Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Nome de usuário"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Senha"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "A autenticação falhou!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Entrar"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Álbuns"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artistas"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr ""
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr ""
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr ""
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr ""
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr ""
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr ""
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr ""
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr ""
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr ""
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr ""
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr ""
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} música selecionada"
msgstr[1] "{} músicas selecionadas"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr ""
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr ""
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr ""
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr ""
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Mais de {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Editora"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Lançamento"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Faixas"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Duração"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr ""
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

389
po/pt.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: pt\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Ver álbum"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Copiar ligação"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Adicionar à fila"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Remover da fila"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Adicionar a {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Não foi possível guardar a palavra-passe. Garanta que o chaveiro da sessão está desbloqueado."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Ocorreu um erro! Verifique os logs para mais detalhes!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Biblioteca"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Listas de reprodução"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "A reproduzir"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Nenhuma canção a ser reproduzida"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Sobre"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Sair"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Terminar sessão"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Conexão restaurada"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Melhores faixas"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Lançamentos"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Autenticar-se no Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Utilizador"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Palavra-passe"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Autenticação falhou!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Autenticar"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Álbums"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Artistas"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Músicas salvas"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Pausar"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Tocar"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Misturar"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Anterior"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Próximo"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Repetir"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Não tem listas de reprodução salvas."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "As tuas listas de reprodução aparecerão aqui."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Não tens albúns salvos."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "A tua biblioteca vai aparecer aqui."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} canção selecionada"
msgstr[1] "{} canções selecionadas"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Procurar no Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Escrever para pesquisar."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Remover"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Adicionar à lista de reprodução..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Cancelar"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Selecionar todos"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Mais de {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Editora"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Lançamento"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Faixas"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Duração"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Copyright"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} por {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Lista de reprodução sem nome"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Todas as listas de reprodução"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Músicas salvas!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Preferências"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Falha ao abrir a ligação!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Áudio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Back-end de Áudio"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "Dispositivo ALSA"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Aplicado apenas se back-end do áudio é ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Qualidade do Áudio"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normal"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Alta"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Muito Alta"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Aparência"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Tema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Claro"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Escuro"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Rede"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Porta do Ponto de Acesso"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Porta usada para conexões ao Ponto de Acesso do Spotify. Definir para 0 se qualquer porta servir."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Salvar na biblioteca"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Nova lista de reprodução criada."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Ver"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Nova lista de reprodução"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Reprodução sem intervalos"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Sistema"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Feito"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Nome"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Criar"

391
po/ru.po Normal file
View file

@ -0,0 +1,391 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: ru\n"
"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"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Посмотреть альбом"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Скопировать ссылку"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Добавить в очередь"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Удалить из очереди"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Добавить в {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Не удалось сохранить пароль. Убедитесь, что связка ключей сеанса разблокирована."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Произошла ошибка. Смотрите подробности в логах!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Фонотека"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Плейлисты"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Воспроизводится сейчас"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Песня сейчас не играет"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "О Spot"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Выйти из программы"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Выйти из аккаунта"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Подключение восстановлено"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Топ треков"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Релизы"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Войти в Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Имя аккаунта"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Пароль"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Вход не удался!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Войти"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Альбомы"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Исполнители"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Сохраненные треки"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Пауза"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Воспроизведение"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Перемешать"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Предыдущий"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Следующий"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Повторить"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "У вас нет сохраненных плейлистов."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Здесь будут показаны ваши плейлисты."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "У вас нет сохраненных альбомов."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Здесь будет показана ваша фонотека."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} композиция выбрана"
msgstr[1] "{} композиции выбраны"
msgstr[2] "{} композиций выбрано"
msgstr[3] ""
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Поиск в Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Введите для поиска."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Удалить"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Добавить в плейлист..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Отменить"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Выбрать все"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Больше от {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Лейбл"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Релизы"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Треки"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Продолжительность"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Авторское право"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} в исполнении {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Безымянный плейлист"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Все плейлисты"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Треки сохранены!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Настройки"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Не удалось открыть ссылку!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Аудио"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Аудио Бэкэнд"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "ALSA Device"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Применяется, только если звуковой бэкэнд ALSA"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Качество звука"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Нормальное"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Высокое"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Очень высокое"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Внешний вид"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Тема"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Светлая"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Темная"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Сеть"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Порт точки доступа"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Порт, используемый для подключения к точке доступа Spotify. Установите на 0, если все порты в порядке."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Сохранить в библиотеку"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Создан новый плейлист."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Вид"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Новый плейлист"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Непрерывное воспроизведение"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Система"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Сделано"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Имя"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Создать"

391
po/sl.po Normal file
View file

@ -0,0 +1,391 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: sl\n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Oglej si album"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Kopiraj povezavo"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Dodaj v vrsto"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Odstrani iz vrste"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Dodaj na {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Gesla ni bilo mogoče shraniti. Preveri, da je session keyring odklenjen."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Napaka! Poglej v dnevnik od programa za podrobnosti."
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Moja zbirka"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Seznami predvajanja"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Se predvaja"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Ni skladb za predvajanje"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "O programu"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Izhod"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Odjava"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Povezava ponovno vzpovstavljena"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Priljubljene skladbe"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Izdaje"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Prijava v Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Uporabniško ime"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Geslo"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Preverjanje pristnosti ni bilo uspešno!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Prijava"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albumi"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Izvajalci"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Shranjene skladbe"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Premor"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Predvajaj"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Naključno predvajanje"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Prejšna skladba"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Naslednja skladba"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Ponavljajoče predvajanje"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Nimaš shranjenih seznamov predvajanja."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Tvoji seznami predvajanja bodo prikazani tukaj."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Nimaš shranjenih albumov."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Tvoja zbirka bo prikazana tukaj."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} izbrana skladba"
msgstr[1] "{} izbrani skladbi"
msgstr[2] "{} izbrane skladbe"
msgstr[3] "{} izbranih skladb"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Razišči Spotify."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Napiši za iskanje."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Odstrani"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Dodaj na seznam predvajanja..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "Prekliči"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Izberi vse"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Več od {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Založba"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Izdano"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Skladbe"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Trajanje"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Avtorska pravica"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{} od {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

418
po/spot.pot Normal file
View file

@ -0,0 +1,418 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the spot package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: spot\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-18 19:15-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:106 src/app/components/mod.rs:133
msgid "An error occured. Check logs for details!"
msgstr ""
#: src/app/components/device_selector/widget.rs:135
#: src/app/components/device_selector/device_selector.blp:9
#: src/app/components/device_selector/device_selector.blp:37
msgid "This device"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr ""
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr ""
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] ""
msgstr[1] ""
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr ""
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr ""
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr ""
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr ""
#. translators: This is a sidebar entry that marks that the entries below are playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr ""
#. translators: This is a sidebar entry to browse to saved tracks.
#: src/app/components/navigation/factory.rs:76
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr ""
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr ""
#. translators: Short text displayed instead of a song title when nothing plays
#. Translators: Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr ""
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr ""
#: src/connect/player.rs:65
msgid "Connection to device lost!"
msgstr ""
#: src/main.rs:76
msgid "Failed to open link!"
msgstr ""
#. Translators: A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr ""
#. Translators: A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr ""
#. Translators: Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr ""
#. Translators: Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr ""
#. Translators: This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr ""
#. Translators: This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr ""
#. Translators: Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr ""
#. Translators: Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr ""
#. Translators: Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Translators: Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Translators: Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Translators: Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Translators: Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Translators: Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#. Translators: Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Translators: Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Translators: Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Translators: Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Translators: Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid ""
"Port used for connections to Spotify's Access Point. Set to 0 if any port is "
"fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. Translators: This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr ""
#. Translators: This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr ""
#. Translators: This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr ""
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr ""
#: src/app/components/login/login.blp:46
msgid "Welcome to Spot"
msgstr ""
#. Translators: Login window title, must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:53
msgid ""
"Log in with your Spotify Account. A Spotify Premium subscription is required "
"to use the app."
msgstr ""
#. Translators: Placeholder for the username field
#: src/app/components/login/login.blp:66
msgid "Username or Email"
msgstr ""
#. Translators: Placeholder for the password field
#: src/app/components/login/login.blp:71
msgid "Password"
msgstr ""
#. Translators: This error is shown when authentication fails.
#: src/app/components/login/login.blp:81
msgid "Incorrect login credentials."
msgstr ""
#. Translators: Log in button label
#: src/app/components/login/login.blp:92
msgid "Log in"
msgstr ""
#. Translators: Exit playlist edition
#. Translators: Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr ""
#. Translators: Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. Translators: Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr ""
#: src/app/components/device_selector/device_selector.blp:15
msgid "Playing on"
msgstr ""
#: src/app/components/device_selector/device_selector.blp:24
msgid "Refresh devices"
msgstr ""
#. Translators: label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Translators: Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr ""
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr ""
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr ""
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr ""
#. Translators: A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr ""
#. Translators: A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr ""

389
po/tr.po Normal file
View file

@ -0,0 +1,389 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: tr\n"
"Plural-Forms: nplurals=2; plural=(n>1);\n"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Albümü görüntüle"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Bağlantıyı kopyala"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Sıraya ekle"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Sıradan çıkar"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "{} listesine ekle"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Parola kaydedilemedi. Oturum anahtarlığınızın kilidinin açık olduğundan emin olun."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Bir hata meydana geldi. Günlükleri kontrol edin."
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Kütüphane"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Oynatma listesi"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Şu an oynatılıyor"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Herhangi bir şarkı oynatılmıyor"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Hakkında"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Çık"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Oturumdan çık"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "Bağlantı geri yüklendi."
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "En çok dinlenenler"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Parçalar"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Spotify Premium'a giriş yap"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Kullanıcı adı"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Parola"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Kimlik doğrulama başarısız."
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Giriş yap"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Albümler"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Sanatçılar"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr "Kaydedilen parçalar"
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr "Durdur"
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr "Oynat"
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr "Karıştır"
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr "Önceki"
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr "Sonraki"
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr "Yinele"
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr "Kaydedilmiş oynatma listeniz yok."
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr "Oynatma listeniz burada gösterilir."
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr "Kaydedilmiş ablümünüz yok."
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr "Kütüphaneniz burada gösterilir."
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] "{} seçildi"
msgstr[1] "{} seçildi"
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr "Spotify'da Ara."
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr "Aramak için yazın."
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr "Kaldır"
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr "Oynatma listesine ekle..."
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr "İptal Et"
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr "Tümünü seç"
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr "Daha fazla {}"
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr "Yayıncı"
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr "Yayın tarihi"
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr "Parça"
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr "Süre"
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr "Telif hakkı"
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr "{}, {}"
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr "Adsız oynatma listesi"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr "Tüm Oynatma Listeleri"
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr "Parçalar kaydedildi!"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr "Tercihler"
#: src/main.rs:75
msgid "Failed to open link!"
msgstr "Bağlantıılamadı!"
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr "Ses"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr "Ses Arka Ucu"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr "ALSA Aygıtı"
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr "Sadece, ses arka ucu ALSA ise uygulanır"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr "Ses Kalitesi"
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr "Normal"
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr "Yüksek"
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr "Çok yüksek"
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr "Görünüm"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr "Tema"
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr "Açık"
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr "Koyu"
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr "Ağ"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr "Erişim Noktası Bağlantı Noktası"
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr "Spotify Erişim Noktasına bağlanmak için kullanılan bağlantı noktası. Herhangi bir bağlantı noktası iyiyse 0'a ayarlayın."
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr "Kütüphaneye kaydet"
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr "Yeni çalma listesi oluşturuldu."
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr "Görünüm"
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr "Yeni Çalma Listesi"
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr "Kesintisiz çalma"
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr "Sistem"
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr "Bitti"
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr "Ad"
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr "Oluştur"

391
po/uk.po Normal file
View file

@ -0,0 +1,391 @@
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: Spot\n"
"Language: uk\n"
"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"
#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track.
#: src/app/components/labels.rs:5
msgid "View album"
msgstr "Подивитися альбом"
#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track.
#: src/app/components/labels.rs:8
msgid "Copy link"
msgstr "Копіювати посилання"
#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue.
#: src/app/components/labels.rs:11
#: src/app/components/selection/selection_toolbar.blp:36
msgid "Add to queue"
msgstr "Додати в чергу"
#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue.
#: src/app/components/labels.rs:14
msgid "Remove from queue"
msgstr "Видалити з черги"
#. translators: This is part of a larger text that says "Add to <playlist name>". This text should be as short as possible.
#: src/app/components/labels.rs:21
msgid "Add to {}"
msgstr "Додати в {}"
#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store).
#: src/app/components/login/login_model.rs:68
msgid "Could not save password. Make sure the session keyring is unlocked."
msgstr "Не вдалося зберегти пароль. Переконайтесь, що зв'язка ключів сеансу розблокована."
#. translators: This notification is the default message for unhandled errors. Logs refer to console output.
#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129
msgid "An error occured. Check logs for details!"
msgstr "Сталася помилка. Перевірте журнал для детальної інформації!"
#. translators: This is a sidebar entry to browse to saved albums.
#: src/app/components/navigation/factory.rs:33
#: src/app/components/sidebar/sidebar_item.rs:39
msgid "Library"
msgstr "Бібліотека"
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/navigation/factory.rs:54
#: src/app/components/sidebar/sidebar_item.rs:45
msgid "Playlists"
msgstr "Плейлисти"
#. This is the visible name for the play queue. It appears in the sidebar as well.
#: src/app/components/now_playing/now_playing_model.rs:134
#: src/app/components/sidebar/sidebar_item.rs:43
msgid "Now playing"
msgstr "Відтворюється зараз"
#. translators: Short text displayed instead of a song title when nothing plays
#. Short text displayed instead of a song title when nothing plays
#: src/app/components/playback/playback_info.rs:58
#: src/app/components/playback/playback_info.blp:33
msgid "No song playing"
msgstr "Пісня зараз не грає"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:67
msgid "About"
msgstr "Про Spot"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:69
msgid "Quit"
msgstr "Вийти з програми"
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:74
msgid "Log out"
msgstr "Вийти з аккаунту"
#: src/app/state/login_state.rs:115
msgid "Connection restored"
msgstr "З'єднання відновлено"
#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify.
#: src/app/components/artist_details/artist_details.blp:26
msgid "Top tracks"
msgstr "Топ треків"
#. Title of the sections that contains all releases from an artist (both singles and albums).
#: src/app/components/artist_details/artist_details.blp:54
msgid "Releases"
msgstr "Релізи"
#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required).
#: src/app/components/login/login.blp:49
msgid "Login to Spotify Premium"
msgstr "Увійти в Spotify Premium"
#. Placeholder for the username field
#: src/app/components/login/login.blp:76
msgid "Username"
msgstr "Логін"
#. Placeholder for the password field
#: src/app/components/login/login.blp:94
msgid "Password"
msgstr "Пароль"
#. This error is shown when authentication fails.
#: src/app/components/login/login.blp:116
msgid "Authentication failed!"
msgstr "Помилка входу!"
#. Log in button label
#: src/app/components/login/login.blp:131
msgid "Log in"
msgstr "Увійти"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:69
msgid "Albums"
msgstr "Альбоми"
#. This is the title of a section of the search results
#: src/app/components/search/search.blp:100
msgid "Artists"
msgstr "Виконавці"
#: src/app/components/navigation/factory.rs:85
#: src/app/components/sidebar/sidebar_item.rs:41
msgid "Saved tracks"
msgstr ""
#: src/app/components/playback/playback_controls.rs:64
msgid "Pause"
msgstr ""
#: src/app/components/playback/playback_controls.rs:66
msgid "Play"
msgstr ""
#: src/app/components/playback/playback_controls.blp:15
msgid "Shuffle"
msgstr ""
#: src/app/components/playback/playback_controls.blp:24
msgid "Previous"
msgstr ""
#: src/app/components/playback/playback_controls.blp:46
msgid "Next"
msgstr ""
#: src/app/components/playback/playback_controls.blp:55
msgid "Repeat"
msgstr ""
#. A title that is shown when the user has not saved any playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:26
msgid "You have no saved playlists."
msgstr ""
#. A description of what happens when the user has saved playlists.
#: src/app/components/saved_playlists/saved_playlists.blp:30
msgid "Your playlists will be shown here."
msgstr ""
#. A title that is shown when the user has not saved any albums.
#: src/app/components/library/library.blp:25
msgid "You have no saved albums."
msgstr ""
#. A description of what happens when the user has saved albums.
#: src/app/components/library/library.blp:29
msgid "Your library will be shown here."
msgstr ""
#. translators: This shows up when in selection mode. This text should be as short as possible.
#: src/app/components/labels.rs:30
msgid "{} song selected"
msgid_plural "{} songs selected"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
#. Title for the empty search page (initial state).
#: src/app/components/search/search.blp:110
msgid "Search Spotify."
msgstr ""
#. Subtitle for the empty search page (initial state).
#: src/app/components/search/search.blp:114
msgid "Type to search."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:61
msgid "Remove"
msgstr ""
#. <property name="icon-name">playlist2-symbolic</property>
#: src/app/components/selection/selection_toolbar.blp:49
msgid "Add to playlist..."
msgstr ""
#. Button label. Exits selection mode.
#: src/app/components/playlist_details/playlist_headerbar.blp:53
#: src/app/components/headerbar/headerbar.blp:49
msgid "Cancel"
msgstr ""
#. Button label. Selects all visible songs.
#: src/app/components/headerbar/headerbar.blp:63
msgid "Select all"
msgstr ""
#. translators: This is part of a contextual menu attached to a single track; the full text is "More from <artist>".
#: src/app/components/labels.rs:39
msgid "More from {}"
msgstr ""
#. This refers to a music label
#: src/app/components/details/release_details.blp:41
msgid "Label"
msgstr ""
#. This refers to a release date
#: src/app/components/details/release_details.blp:52
msgid "Released"
msgstr ""
#. This refers to a number of tracks
#: src/app/components/details/release_details.blp:63
msgid "Tracks"
msgstr ""
#. This refers to the duration of a release
#: src/app/components/details/release_details.ui:68
msgid "Duration"
msgstr ""
#: src/app/components/details/release_details.blp:72
msgid "Copyright"
msgstr ""
#. translators: This is part of a larger label that reads "<Album> by <Artist>"
#: src/app/components/labels.rs:48
msgid "{} by {}"
msgstr ""
#: src/app/components/sidebar/sidebar.rs:49
msgid "Unnamed playlist"
msgstr ""
#. translators: This is a sidebar entry to browse to saved playlists.
#: src/app/components/sidebar/sidebar_item.rs:81
msgid "All Playlists"
msgstr ""
#: src/app/components/selection/component.rs:66
msgid "Tracks saved!"
msgstr ""
#. translators: This is a menu entry.
#: src/app/components/user_menu/user_menu.rs:65
msgid "Preferences"
msgstr ""
#: src/main.rs:75
msgid "Failed to open link!"
msgstr ""
#. Header for a group of preference items regarding audio
#: src/app/components/settings/settings.blp:13
msgid "Audio"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:18
msgid "Audio Backend"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:30
msgid "ALSA Device"
msgstr ""
#. Description for the item (ALSA Device) in preferences
#: src/app/components/settings/settings.blp:34
msgid "Applied only if audio backend is ALSA"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:44
msgid "Audio Quality"
msgstr ""
#: src/app/components/settings/settings.blp:47
msgid "Normal"
msgstr ""
#: src/app/components/settings/settings.blp:48
msgid "High"
msgstr ""
#: src/app/components/settings/settings.blp:49
msgid "Very high"
msgstr ""
#. Header for a group of preference items regarding the application's appearance
#: src/app/components/settings/settings.blp:72
msgid "Appearance"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:77
msgid "Theme"
msgstr ""
#: src/app/components/settings/settings.blp:80
msgid "Light"
msgstr ""
#: src/app/components/settings/settings.blp:81
msgid "Dark"
msgstr ""
#. Header for a group of preference items regarding network
#: src/app/components/settings/settings.blp:91
msgid "Network"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:96
msgid "Access Point Port"
msgstr ""
#. Longer description for an item (Access Point Port) in preferences
#: src/app/components/settings/settings.blp:100
msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine."
msgstr ""
#: src/app/components/selection/selection_toolbar.blp:78
msgid "Save to library"
msgstr ""
#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist.
#: src/app/components/notification/mod.rs:25
msgid "New playlist created."
msgstr ""
#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened.
#: src/app/components/notification/mod.rs:27
msgid "View"
msgstr ""
#: src/app/components/sidebar/sidebar_item.rs:90
msgid "New Playlist"
msgstr ""
#. Title for an item in preferences
#: src/app/components/settings/settings.blp:57
msgid "Gapless playback"
msgstr ""
#: src/app/components/settings/settings.blp:82
msgid "System"
msgstr ""
#. Finish playlist edition
#: src/app/components/playlist_details/playlist_headerbar.blp:69
msgid "Done"
msgstr ""
#. label for the entry containing the name of a new playlist
#: src/app/components/sidebar/create_playlist.blp:10
msgid "Name"
msgstr ""
#. Button that creates a new playlist
#: src/app/components/sidebar/create_playlist.blp:30
msgid "Create"
msgstr ""

3
rustfmt.toml Normal file
View file

@ -0,0 +1,3 @@
#use_small_heuristics = "Max"
newline_style = "Unix"
use_field_init_shorthand = true

73
spot.nix Normal file
View file

@ -0,0 +1,73 @@
{ lib
, stdenv
, fetchgit
, nix-update-script
, meson
, ninja
, gettext
, desktop-file-utils
, cargo
, rustPlatform
, rustc
, pkg-config
, glib
, libadwaita
, libhandy
, gtk4
, openssl
, alsa-lib
, libpulseaudio
, wrapGAppsHook4
, blueprint-compiler
, gst_all_1
}:
rustPlatform.buildRustPackage rec {
pname = "spot";
version = "0.6.0";
src = fetchgit {
url = "https://github.com/stevenleadbeater/spot.git";
rev = "b2291e0834d9a1a6e7ce1020fc88c483dfbc0b24";
hash = "sha256-JFBwTAYKBwmjUI3B67zB60dLM+DjIzzbO/1iLsn+5TE=";
};
cargoHash = "sha256-HIjA+oDFegsnnV3EViaWfsNhmqPB1yl8rIVS5LbTu/A=";
nativeBuildInputs = [
gettext
meson
ninja
pkg-config
gtk4 # for gtk-update-icon-cache
glib # for glib-compile-schemas
desktop-file-utils
cargo
rustPlatform.cargoSetupHook
rustc
wrapGAppsHook4
blueprint-compiler
];
buildInputs = [
glib
gtk4
libadwaita
libhandy
openssl
alsa-lib
libpulseaudio
gst_all_1.gst-plugins-base
gst_all_1.gstreamer
];
# https://github.com/xou816/spot/issues/313
mesonBuildType = "release";
meta = {
description = "Native Spotify client for the GNOME desktop";
mainProgram = "spot";
homepage = "https://github.com/stevenleadbeater/spot";
maintainers = [];
};
}

689
src/api/api_models.rs Normal file
View file

@ -0,0 +1,689 @@
use form_urlencoded::Serializer;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
collections::HashSet,
convert::{Into, TryFrom, TryInto},
vec::IntoIter,
};
use crate::app::{models::*, SongsSource};
#[derive(Serialize)]
pub struct PlaylistDetails {
pub name: String,
}
#[derive(Serialize)]
pub struct Uris {
pub uris: Vec<String>,
}
#[derive(Serialize)]
pub struct PlayOffset {
pub position: u32,
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum PlayRequest {
Contextual {
context_uri: String,
offset: PlayOffset,
},
Uris {
uris: Vec<String>,
offset: PlayOffset,
},
}
#[derive(Serialize)]
pub struct Ids {
pub ids: Vec<String>,
}
#[derive(Serialize)]
pub struct Name<'a> {
pub name: &'a str,
}
pub enum SearchType {
Artist,
Album,
}
impl SearchType {
fn into_string(self) -> &'static str {
match self {
Self::Artist => "artist",
Self::Album => "album",
}
}
}
pub struct SearchQuery {
pub query: String,
pub types: Vec<SearchType>,
pub limit: usize,
pub offset: usize,
}
impl SearchQuery {
pub fn into_query_string(self) -> String {
let mut types = self
.types
.into_iter()
.fold(String::new(), |acc, t| acc + t.into_string() + ",");
types.pop();
let re = Regex::new(r"(\W|\s)+").unwrap();
let query = re.replace_all(&self.query[..], " ");
let serialized = Serializer::new(String::new())
.append_pair("q", query.as_ref())
.append_pair("offset", &self.offset.to_string()[..])
.append_pair("limit", &self.limit.to_string()[..])
.append_pair("market", "from_token")
.finish();
format!("type={types}&{serialized}")
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct Page<T> {
items: Option<Vec<T>>,
offset: Option<usize>,
limit: Option<usize>,
total: usize,
}
impl<T> Page<T> {
fn new(items: Vec<T>) -> Self {
let l = items.len();
Self {
total: l,
items: Some(items),
offset: Some(0),
limit: Some(l),
}
}
fn map<Mapper, U>(self, mapper: Mapper) -> Page<U>
where
Mapper: Fn(T) -> U,
{
let Page {
items,
offset,
limit,
total,
} = self;
Page {
items: items.map(|item| item.into_iter().map(mapper).collect()),
offset,
limit,
total,
}
}
pub fn limit(&self) -> usize {
self.limit
.or_else(|| Some(self.items.as_ref()?.len()))
.filter(|limit| *limit > 0)
.unwrap_or(50)
}
pub fn total(&self) -> usize {
self.total
}
pub fn offset(&self) -> usize {
self.offset.unwrap_or(0)
}
}
impl<T> IntoIterator for Page<T> {
type Item = T;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.items.unwrap_or_default().into_iter()
}
}
impl<T> Default for Page<T> {
fn default() -> Self {
Self {
items: None,
total: 0,
offset: Some(0),
limit: Some(0),
}
}
}
trait WithImages {
fn images(&self) -> &[Image];
fn best_image<T: PartialOrd, F: Fn(&Image) -> T>(&self, criterion: F) -> Option<&Image> {
let mut ords = self
.images()
.iter()
.map(|image| (criterion(image), image))
.collect::<Vec<(T, &Image)>>();
ords.sort_by(|a, b| (a.0).partial_cmp(&b.0).unwrap());
Some(ords.first()?.1)
}
fn best_image_for_width(&self, width: i32) -> Option<&Image> {
self.best_image(|i| (width - i.width.unwrap_or(0) as i32).abs())
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct Playlist {
pub id: String,
pub name: String,
pub images: Vec<Image>,
pub tracks: Page<PlaylistTrack>,
pub owner: PlaylistOwner,
}
#[derive(Deserialize, Debug, Clone)]
pub struct PlaylistOwner {
pub id: String,
pub display_name: String,
}
impl WithImages for Playlist {
fn images(&self) -> &[Image] {
&self.images[..]
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct PlaylistTrack {
pub is_local: bool,
pub track: Option<FailibleTrackItem>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct SavedTrack {
pub added_at: String,
pub track: TrackItem,
}
#[derive(Deserialize, Debug, Clone)]
pub struct SavedAlbum {
pub album: Album,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FullAlbum {
#[serde(flatten)]
pub album: Album,
#[serde(flatten)]
pub album_info: AlbumInfo,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Album {
pub id: String,
pub tracks: Option<Page<AlbumTrackItem>>,
pub artists: Vec<Artist>,
pub release_date: Option<String>,
pub name: String,
pub images: Vec<Image>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct AlbumInfo {
pub label: String,
pub copyrights: Vec<Copyright>,
pub total_tracks: u32,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Copyright {
pub text: String,
#[serde(alias = "type")]
pub type_: char,
}
impl WithImages for Album {
fn images(&self) -> &[Image] {
&self.images[..]
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct Image {
pub url: String,
pub height: Option<u32>,
pub width: Option<u32>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Artist {
pub id: String,
pub name: String,
pub images: Option<Vec<Image>>,
}
impl WithImages for Artist {
fn images(&self) -> &[Image] {
#[allow(clippy::manual_unwrap_or_default)]
if let Some(ref images) = self.images {
images
} else {
&[]
}
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct User {
pub id: String,
pub display_name: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Device {
#[serde(alias = "type")]
pub type_: String,
pub name: String,
pub id: String,
pub is_active: bool,
pub is_restricted: bool,
pub volume_percent: u32,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Devices {
pub devices: Vec<Device>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct PlayerQueue {
pub currently_playing: TrackItem,
pub queue: Vec<TrackItem>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct PlayerContext {
#[serde(alias = "type")]
pub type_: String,
pub uri: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct PlayerState {
pub progress_ms: u32,
pub is_playing: bool,
pub repeat_state: String,
pub shuffle_state: bool,
pub item: FailibleTrackItem,
pub context: Option<PlayerContext>,
}
impl From<PlayerState> for ConnectPlayerState {
fn from(
PlayerState {
progress_ms,
is_playing,
repeat_state,
shuffle_state,
item,
context,
}: PlayerState,
) -> Self {
let repeat = match &repeat_state[..] {
"track" => RepeatMode::Song,
"context" => RepeatMode::Playlist,
_ => RepeatMode::None,
};
let source = context.and_then(|PlayerContext { type_, uri }| match type_.as_str() {
"album" => {
let id = uri.split(':').last().unwrap_or_default();
Some(SongsSource::Album(id.to_string()))
}
_ => None,
});
let shuffle = shuffle_state;
let current_song_id = item.get().map(|i| i.track.id);
Self {
is_playing,
progress_ms,
repeat,
shuffle,
source,
current_song_id,
}
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct TopTracks {
pub tracks: Vec<TrackItem>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct AlbumTrackItem {
pub id: String,
pub track_number: Option<usize>,
pub uri: String,
pub name: String,
pub duration_ms: i64,
pub artists: Vec<Artist>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct TrackItem {
#[serde(flatten)]
pub track: AlbumTrackItem,
pub album: Album,
}
#[derive(Deserialize, Debug, Clone)]
pub struct BadTrackItem {}
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum FailibleTrackItem {
Ok(Box<TrackItem>),
Failing(BadTrackItem),
}
impl FailibleTrackItem {
fn get(self) -> Option<TrackItem> {
match self {
Self::Ok(track) => Some(*track),
Self::Failing(_) => None,
}
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct RawSearchResults {
pub albums: Option<Page<Album>>,
pub artists: Option<Page<Artist>>,
}
impl From<Artist> for ArtistSummary {
fn from(artist: Artist) -> Self {
let photo = artist.best_image_for_width(200).map(|i| &i.url).cloned();
let Artist { id, name, .. } = artist;
Self { id, name, photo }
}
}
impl TryFrom<PlaylistTrack> for TrackItem {
type Error = ();
fn try_from(PlaylistTrack { is_local, track }: PlaylistTrack) -> Result<Self, Self::Error> {
track.ok_or(())?.get().filter(|_| !is_local).ok_or(())
}
}
impl From<SavedTrack> for TrackItem {
fn from(track: SavedTrack) -> Self {
track.track
}
}
impl From<PlayerQueue> for Vec<SongDescription> {
fn from(
PlayerQueue {
mut queue,
currently_playing,
}: PlayerQueue,
) -> Self {
let mut ids = HashSet::<String>::new();
queue.insert(0, currently_playing);
let queue: Vec<TrackItem> = queue
.into_iter()
.take_while(|e| {
if ids.contains(&e.track.id) {
false
} else {
ids.insert(e.track.id.clone());
true
}
})
.collect();
Page::new(queue).into()
}
}
impl From<TopTracks> for Vec<SongDescription> {
fn from(top_tracks: TopTracks) -> Self {
Page::new(top_tracks.tracks).into()
}
}
impl<T> From<Page<T>> for Vec<SongDescription>
where
T: TryInto<TrackItem>,
{
fn from(page: Page<T>) -> Self {
SongBatch::from(page).songs
}
}
impl From<(Page<AlbumTrackItem>, &Album)> for SongBatch {
fn from(page_and_album: (Page<AlbumTrackItem>, &Album)) -> Self {
let (page, album) = page_and_album;
Self::from(page.map(|track| TrackItem {
track,
album: album.clone(),
}))
}
}
impl<T> From<Page<T>> for SongBatch
where
T: TryInto<TrackItem>,
{
fn from(page: Page<T>) -> Self {
let batch = Batch {
offset: page.offset(),
batch_size: page.limit(),
total: page.total(),
};
let songs = page
.into_iter()
.filter_map(|t| {
let TrackItem { track, album } = t.try_into().ok()?;
let AlbumTrackItem {
artists,
id,
uri,
name,
duration_ms,
track_number,
} = track;
let artists = artists
.into_iter()
.map(|a| ArtistRef {
id: a.id,
name: a.name,
})
.collect::<Vec<ArtistRef>>();
let art = album.best_image_for_width(200).map(|i| &i.url).cloned();
let Album {
id: album_id,
name: album_name,
..
} = album;
let album_ref = AlbumRef {
id: album_id,
name: album_name,
};
Some(SongDescription {
id,
track_number: track_number.map(|u| u as u32),
uri,
title: name,
artists,
album: album_ref,
duration: duration_ms as u32,
art,
})
})
.collect();
SongBatch { songs, batch }
}
}
impl TryFrom<Album> for SongBatch {
type Error = ();
fn try_from(mut album: Album) -> Result<Self, Self::Error> {
let tracks = album.tracks.take().ok_or(())?;
Ok((tracks, &album).into())
}
}
impl From<FullAlbum> for AlbumFullDescription {
fn from(full_album: FullAlbum) -> Self {
let description = full_album.album.into();
let release_details = full_album.album_info.into();
Self {
description,
release_details,
}
}
}
impl From<Album> for AlbumDescription {
fn from(album: Album) -> Self {
let artists = album
.artists
.iter()
.map(|a| ArtistRef {
id: a.id.clone(),
name: a.name.clone(),
})
.collect::<Vec<ArtistRef>>();
let songs = album
.clone()
.try_into()
.unwrap_or_else(|_| SongBatch::empty());
let art = album.best_image_for_width(200).map(|i| i.url.clone());
Self {
id: album.id,
title: album.name,
artists,
release_date: album.release_date,
art,
songs,
is_liked: false,
}
}
}
impl From<AlbumInfo> for AlbumReleaseDetails {
fn from(
AlbumInfo {
label,
copyrights,
total_tracks,
}: AlbumInfo,
) -> Self {
let copyright_text = copyrights
.iter()
.map(|Copyright { type_, text }| format!("[{type_}] {text}"))
.collect::<Vec<String>>()
.join(",\n ");
Self {
label,
copyright_text,
total_tracks: total_tracks as usize,
}
}
}
impl From<Playlist> for PlaylistDescription {
fn from(playlist: Playlist) -> Self {
let art = playlist.best_image_for_width(200).map(|i| i.url.clone());
let Playlist {
id,
name,
tracks,
owner,
..
} = playlist;
let PlaylistOwner {
id: owner_id,
display_name,
} = owner;
let song_batch = tracks.into();
PlaylistDescription {
id,
title: name,
art,
songs: song_batch,
owner: UserRef {
id: owner_id,
display_name,
},
}
}
}
impl From<Device> for ConnectDevice {
fn from(
Device {
id, name, type_, ..
}: Device,
) -> Self {
let kind = match type_.to_lowercase().as_str() {
"smartphone" => ConnectDeviceKind::Phone,
"computer" => ConnectDeviceKind::Computer,
"speaker" => ConnectDeviceKind::Speaker,
_ => ConnectDeviceKind::Other,
};
Self {
id,
label: name,
kind,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_playlist_track_null() {
let track = r#"{"is_local": false, "track": null}"#;
let deserialized: PlaylistTrack = serde_json::from_str(track).unwrap();
let track_item: Option<TrackItem> = deserialized.try_into().ok();
assert!(track_item.is_none());
}
#[test]
fn test_playlist_track_local() {
let track = r#"{"is_local": true, "track": {"name": ""}}"#;
let deserialized: PlaylistTrack = serde_json::from_str(track).unwrap();
let track_item: Option<TrackItem> = deserialized.try_into().ok();
assert!(track_item.is_none());
}
#[test]
fn test_playlist_track_ok() {
let track = r#"{"is_local":false,"track":{"album":{"artists":[{"external_urls":{"spotify":""},"href":"","id":"","name":"","type":"artist","uri":""}],"id":"","images":[{"height":64,"url":"","width":64}],"name":""},"artists":[{"id":"","name":""}],"duration_ms":1,"id":"","name":"","uri":""}}"#;
let deserialized: PlaylistTrack = serde_json::from_str(track).unwrap();
let track_item: Option<TrackItem> = deserialized.try_into().ok();
assert!(track_item.is_some());
}
}

289
src/api/cache.rs Normal file
View file

@ -0,0 +1,289 @@
use async_std::fs;
use async_std::io;
use async_std::path::Path;
use async_std::path::PathBuf;
use async_std::prelude::*;
use core::mem::size_of;
use futures::join;
use regex::Regex;
use std::convert::From;
use std::future::Future;
use std::time::{Duration, SystemTime};
use thiserror::Error;
const EXPIRY_FILE_EXT: &str = ".expiry";
#[derive(Error, Debug)]
pub enum CacheError {
#[error("No content available")]
NoContent,
#[error("File could not be saved to cache: {0}")]
WriteError(std::io::Error),
#[error("File could not be read from cache: {0}")]
ReadError(std::io::Error),
#[error("File could not be removed from cache: {0}")]
RemoveError(std::io::Error),
#[error(transparent)]
ConversionError(#[from] std::string::FromUtf8Error),
}
pub type ETag = String;
pub enum CacheFile {
Fresh(Vec<u8>),
Expired(Vec<u8>, Option<ETag>),
None,
}
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum CachePolicy {
Default, // query remote cache when stale
IgnoreExpiry, // always use cached value
Revalidate, // always query remote cache
IgnoreCached, // ignore cache alltogether
}
#[derive(PartialEq, Clone, Debug)]
pub enum CacheExpiry {
Never,
AtUnixTimestamp(Duration, Option<ETag>),
}
impl CacheExpiry {
pub fn expire_in_seconds(seconds: u64, etag: Option<ETag>) -> Self {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
Self::AtUnixTimestamp(timestamp + Duration::new(seconds, 0), etag)
}
fn is_expired(&self) -> bool {
match self {
Self::Never => false,
Self::AtUnixTimestamp(ref duration, _) => {
let now = &SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
now > duration
}
}
}
fn etag(&self) -> Option<&String> {
match self {
Self::Never => None,
Self::AtUnixTimestamp(_, ref etag) => etag.as_ref(),
}
}
}
#[derive(Clone)]
pub struct CacheManager {
root: PathBuf,
}
impl CacheManager {
pub fn for_dir(dir: &str) -> Option<Self> {
let root: PathBuf = glib::user_cache_dir().into();
let root = root.join(dir);
let mask = 0o744;
glib::mkdir_with_parents(&root, mask);
Some(Self { root })
}
fn cache_path(&self, resource: &str) -> PathBuf {
self.root.join(resource)
}
fn cache_meta_path(&self, resource: &str) -> PathBuf {
let full = resource.to_string() + EXPIRY_FILE_EXT;
self.root.join(full)
}
}
impl CacheManager {
async fn read_expiry_file(&self, resource: &str) -> Result<CacheExpiry, CacheError> {
let expiry_file = self.cache_meta_path(resource);
match fs::read(&expiry_file).await {
Err(e) => match e.kind() {
io::ErrorKind::NotFound => Ok(CacheExpiry::Never),
_ => Err(CacheError::ReadError(e)),
},
Ok(buffer) => {
const OFFSET: usize = size_of::<u64>();
let mut duration: [u8; OFFSET] = Default::default();
duration.copy_from_slice(&buffer[..OFFSET]);
let duration = Duration::from_secs(u64::from_be_bytes(duration));
let etag = String::from_utf8(buffer[OFFSET..].to_vec()).ok();
Ok(CacheExpiry::AtUnixTimestamp(duration, etag))
}
}
}
pub async fn read_cache_file(
&self,
resource: &str,
policy: CachePolicy,
) -> Result<CacheFile, CacheError> {
if matches!(policy, CachePolicy::IgnoreCached) {
return Ok(CacheFile::None);
}
let path = self.cache_path(resource);
let (file, expiry) = join!(fs::read(&path), self.read_expiry_file(resource));
match (file, policy) {
(Ok(buf), CachePolicy::IgnoreExpiry) => Ok(CacheFile::Fresh(buf)),
(Ok(buf), CachePolicy::Revalidate) => {
let expiry = expiry.unwrap_or(CacheExpiry::Never);
let etag = expiry.etag().cloned();
Ok(CacheFile::Expired(buf, etag))
}
(Ok(buf), CachePolicy::Default) => {
let expiry = expiry?;
let etag = expiry.etag().cloned();
Ok(if expiry.is_expired() {
CacheFile::Expired(buf, etag)
} else {
CacheFile::Fresh(buf)
})
}
(_, CachePolicy::IgnoreCached) => Ok(CacheFile::None),
(Err(e), _) => match e.kind() {
io::ErrorKind::NotFound => Ok(CacheFile::None),
_ => Err(CacheError::ReadError(e)),
},
}
}
}
impl CacheManager {
async fn set_expiry_for_path(
&self,
path: &PathBuf,
expiry: CacheExpiry,
) -> Result<(), CacheError> {
if let CacheExpiry::AtUnixTimestamp(duration, etag) = expiry {
let mut content = duration.as_secs().to_be_bytes().to_vec();
if let Some(etag) = etag {
content.append(&mut etag.into_bytes());
}
fs::write(path, content)
.await
.map_err(CacheError::WriteError)?;
}
Ok(())
}
pub async fn clear_cache_pattern(&self, regex: &Regex) -> Result<(), CacheError> {
let mut entries = fs::read_dir(&self.root)
.await
.map_err(CacheError::ReadError)?;
while let Some(Ok(entry)) = entries.next().await {
let matches = entry
.file_name()
.to_str()
.map(|s| regex.is_match(s))
.unwrap_or(false);
if matches {
info!("Removing {}...", entry.file_name().to_str().unwrap_or(""));
fs::remove_file(entry.path())
.await
.map_err(CacheError::RemoveError)?;
if let Some(expiry_file_path) = entry
.path()
.to_str()
.map(|path| path.to_string() + EXPIRY_FILE_EXT)
{
let _ = fs::remove_file(Path::new(&expiry_file_path)).await;
}
}
}
Ok(())
}
pub async fn set_expired_pattern(&self, regex: &Regex) -> Result<(), CacheError> {
let mut entries = fs::read_dir(&self.root)
.await
.map_err(CacheError::ReadError)?;
while let Some(Ok(entry)) = entries.next().await {
let matches = entry
.file_name()
.to_str()
.and_then(|s| s.strip_suffix(EXPIRY_FILE_EXT))
.map(|s| regex.is_match(s))
.unwrap_or(false);
if matches {
self.set_expiry_for_path(&entry.path(), CacheExpiry::expire_in_seconds(0, None))
.await?;
}
}
Ok(())
}
pub async fn write_cache_file(
&self,
resource: &str,
content: &[u8],
expiry: CacheExpiry,
) -> Result<(), CacheError> {
let file = self.cache_path(resource);
let meta = self.cache_meta_path(resource);
let (r1, r2) = join!(
fs::write(&file, content),
self.set_expiry_for_path(&meta, expiry)
);
r1.map_err(CacheError::WriteError)?;
r2?;
Ok(())
}
pub async fn get_or_write<O, F, E>(
&self,
resource: &str,
policy: CachePolicy,
fetch: F,
) -> Result<Vec<u8>, E>
where
O: Future<Output = Result<FetchResult, E>>,
F: FnOnce(Option<ETag>) -> O,
E: From<CacheError>,
{
let file = self.read_cache_file(resource, policy).await?;
match file {
CacheFile::Fresh(buf) => Ok(buf),
CacheFile::Expired(buf, etag) => match fetch(etag).await? {
FetchResult::NotModified(expiry) => {
let meta = self.cache_meta_path(resource);
self.set_expiry_for_path(&meta, expiry).await?;
Ok(buf)
}
FetchResult::Modified(fresh, expiry) => {
self.write_cache_file(resource, &fresh, expiry).await?;
Ok(fresh)
}
},
CacheFile::None => match fetch(None).await? {
FetchResult::NotModified(_) => Err(E::from(CacheError::NoContent)),
FetchResult::Modified(fresh, expiry) => {
self.write_cache_file(resource, &fresh, expiry).await?;
Ok(fresh)
}
},
}
}
}
pub enum FetchResult {
NotModified(CacheExpiry),
Modified(Vec<u8>, CacheExpiry),
}

876
src/api/cached_client.rs Normal file
View file

@ -0,0 +1,876 @@
use futures::future::BoxFuture;
use futures::{join, FutureExt};
use regex::Regex;
use serde::de::DeserializeOwned;
use serde_json::from_slice;
use std::convert::Into;
use std::future::Future;
use super::cache::{CacheExpiry, CacheManager, CachePolicy, FetchResult};
use super::client::*;
use crate::app::models::*;
pub type SpotifyResult<T> = Result<T, SpotifyApiError>;
pub trait SpotifyApiClient {
fn get_artist(&self, id: &str) -> BoxFuture<SpotifyResult<ArtistDescription>>;
fn get_album(&self, id: &str) -> BoxFuture<SpotifyResult<AlbumFullDescription>>;
fn get_album_tracks(
&self,
id: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<SongBatch>>;
fn get_playlist(&self, id: &str) -> BoxFuture<SpotifyResult<PlaylistDescription>>;
fn get_playlist_tracks(
&self,
id: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<SongBatch>>;
fn get_saved_albums(
&self,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Vec<AlbumDescription>>>;
fn get_saved_tracks(&self, offset: usize, limit: usize) -> BoxFuture<SpotifyResult<SongBatch>>;
fn save_album(&self, id: &str) -> BoxFuture<SpotifyResult<AlbumDescription>>;
fn save_tracks(&self, ids: Vec<String>) -> BoxFuture<SpotifyResult<()>>;
fn remove_saved_album(&self, id: &str) -> BoxFuture<SpotifyResult<()>>;
fn remove_saved_tracks(&self, ids: Vec<String>) -> BoxFuture<SpotifyResult<()>>;
fn get_saved_playlists(
&self,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Vec<PlaylistDescription>>>;
fn add_to_playlist(&self, id: &str, uris: Vec<String>) -> BoxFuture<SpotifyResult<()>>;
fn create_new_playlist(
&self,
name: &str,
user_id: &str,
) -> BoxFuture<SpotifyResult<PlaylistDescription>>;
fn remove_from_playlist(&self, id: &str, uris: Vec<String>) -> BoxFuture<SpotifyResult<()>>;
fn update_playlist_details(&self, id: &str, name: String) -> BoxFuture<SpotifyResult<()>>;
fn search(
&self,
query: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<SearchResults>>;
fn get_artist_albums(
&self,
id: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Vec<AlbumDescription>>>;
fn get_user(&self, id: &str) -> BoxFuture<SpotifyResult<UserDescription>>;
fn get_user_playlists(
&self,
id: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Vec<PlaylistDescription>>>;
fn list_available_devices(&self) -> BoxFuture<SpotifyResult<Vec<ConnectDevice>>>;
fn get_player_queue(&self) -> BoxFuture<SpotifyResult<Vec<SongDescription>>>;
fn update_token(&self, token: String);
fn player_pause(&self, device_id: String) -> BoxFuture<SpotifyResult<()>>;
fn player_resume(&self, device_id: String) -> BoxFuture<SpotifyResult<()>>;
#[allow(dead_code)]
fn player_next(&self, device_id: String) -> BoxFuture<SpotifyResult<()>>;
fn player_seek(&self, device_id: String, pos: usize) -> BoxFuture<SpotifyResult<()>>;
fn player_repeat(&self, device_id: String, mode: RepeatMode) -> BoxFuture<SpotifyResult<()>>;
fn player_shuffle(&self, device_id: String, shuffle: bool) -> BoxFuture<SpotifyResult<()>>;
fn player_volume(&self, device_id: String, volume: u8) -> BoxFuture<SpotifyResult<()>>;
fn player_play_in_context(
&self,
device_id: String,
context: String,
offset: usize,
) -> BoxFuture<SpotifyResult<()>>;
fn player_play_no_context(
&self,
device_id: String,
uris: Vec<String>,
offset: usize,
) -> BoxFuture<SpotifyResult<()>>;
fn player_state(&self) -> BoxFuture<SpotifyResult<ConnectPlayerState>>;
}
enum SpotCacheKey<'a> {
SavedAlbums(usize, usize),
SavedTracks(usize, usize),
SavedPlaylists(usize, usize),
Album(&'a str),
AlbumLiked(&'a str),
AlbumTracks(&'a str, usize, usize),
Playlist(&'a str),
PlaylistTracks(&'a str, usize, usize),
ArtistAlbums(&'a str, usize, usize),
Artist(&'a str),
ArtistTopTracks(&'a str),
User(&'a str),
UserPlaylists(&'a str, usize, usize),
}
impl<'a> SpotCacheKey<'a> {
fn into_raw(self) -> String {
match self {
Self::SavedAlbums(offset, limit) => format!("me_albums_{offset}_{limit}.json"),
Self::SavedTracks(offset, limit) => format!("me_tracks_{offset}_{limit}.json"),
Self::SavedPlaylists(offset, limit) => format!("me_playlists_{offset}_{limit}.json"),
Self::Album(id) => format!("album_{id}.json"),
Self::AlbumTracks(id, offset, limit) => {
format!("album_item_{id}_{offset}_{limit}.json")
}
Self::AlbumLiked(id) => format!("album_liked_{id}.json"),
Self::Playlist(id) => format!("playlist_{id}.json"),
Self::PlaylistTracks(id, offset, limit) => {
format!("playlist_item_{id}_{offset}_{limit}.json")
}
Self::ArtistAlbums(id, offset, limit) => {
format!("artist_albums_{id}_{offset}_{limit}.json")
}
Self::Artist(id) => format!("artist_{id}.json"),
Self::ArtistTopTracks(id) => format!("artist_top_tracks_{id}.json"),
Self::User(id) => format!("user_{id}.json"),
Self::UserPlaylists(id, offset, limit) => {
format!("user_playlists_{id}_{offset}_{limit}.json")
}
}
}
}
lazy_static! {
pub static ref ME_TRACKS_CACHE: Regex = Regex::new(r"^me_tracks_\w+_\w+\.json$").unwrap();
pub static ref ME_ALBUMS_CACHE: Regex = Regex::new(r"^me_albums_\w+_\w+\.json$").unwrap();
pub static ref USER_CACHE: Regex =
Regex::new(r"^me_(albums|playlists|tracks)_\w+_\w+\.json$").unwrap();
}
fn playlist_cache_key(id: &str) -> Regex {
Regex::new(&format!(r"^playlist(_{id}|item_{id}_\w+_\w+)\.json$")).unwrap()
}
pub struct CachedSpotifyClient {
client: SpotifyClient,
cache: CacheManager,
}
impl CachedSpotifyClient {
pub fn new() -> CachedSpotifyClient {
CachedSpotifyClient {
client: SpotifyClient::new(),
cache: CacheManager::for_dir("spot/net").unwrap(),
}
}
fn default_cache_policy(&self) -> CachePolicy {
if self.client.has_token() {
CachePolicy::Default
} else {
CachePolicy::IgnoreExpiry
}
}
async fn wrap_write<T, O, F>(write: &F, etag: Option<String>) -> SpotifyResult<FetchResult>
where
O: Future<Output = SpotifyResult<SpotifyResponse<T>>>,
F: Fn(Option<String>) -> O,
{
write(etag)
.map(|r| {
let SpotifyResponse {
kind,
max_age,
etag,
} = r?;
let expiry = CacheExpiry::expire_in_seconds(max_age, etag);
SpotifyResult::Ok(match kind {
SpotifyResponseKind::Ok(content, _) => {
FetchResult::Modified(content.into_bytes(), expiry)
}
SpotifyResponseKind::NotModified => FetchResult::NotModified(expiry),
})
})
.await
}
async fn cache_get_or_write<T, O, F>(
&self,
key: SpotCacheKey<'_>,
cache_policy: Option<CachePolicy>,
write: F,
) -> SpotifyResult<T>
where
O: Future<Output = SpotifyResult<SpotifyResponse<T>>>,
F: Fn(Option<String>) -> O,
T: DeserializeOwned,
{
let write = &write;
let cache_key = key.into_raw();
let raw = self
.cache
.get_or_write(
&cache_key,
cache_policy.unwrap_or_else(|| self.default_cache_policy()),
|etag| Self::wrap_write(write, etag),
)
.await?;
let result = from_slice::<T>(&raw);
match result {
Ok(t) => Ok(t),
// parsing failed: cache is likely invalid, request again, ignoring cache
Err(e) => {
dbg!(&cache_key, e);
let new_raw = self
.cache
.get_or_write(&cache_key, CachePolicy::IgnoreCached, |etag| {
Self::wrap_write(write, etag)
})
.await?;
Ok(from_slice::<T>(&new_raw)?)
}
}
}
}
impl SpotifyApiClient for CachedSpotifyClient {
fn update_token(&self, new_token: String) {
self.client.update_token(new_token)
}
fn get_saved_albums(
&self,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Vec<AlbumDescription>>> {
Box::pin(async move {
let page = self
.cache_get_or_write(SpotCacheKey::SavedAlbums(offset, limit), None, |etag| {
self.client
.get_saved_albums(offset, limit)
.etag(etag)
.send()
})
.await?;
let albums = page
.into_iter()
.map(|saved| saved.album.into())
.collect::<Vec<AlbumDescription>>();
Ok(albums)
})
}
fn get_saved_tracks(&self, offset: usize, limit: usize) -> BoxFuture<SpotifyResult<SongBatch>> {
Box::pin(async move {
let page = self
.cache_get_or_write(SpotCacheKey::SavedTracks(offset, limit), None, |etag| {
self.client
.get_saved_tracks(offset, limit)
.etag(etag)
.send()
})
.await?;
Ok(page.into())
})
}
fn get_saved_playlists(
&self,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Vec<PlaylistDescription>>> {
Box::pin(async move {
let page = self
.cache_get_or_write(SpotCacheKey::SavedPlaylists(offset, limit), None, |etag| {
self.client
.get_saved_playlists(offset, limit)
.etag(etag)
.send()
})
.await?;
let albums = page
.into_iter()
.map(|playlist| playlist.into())
.collect::<Vec<PlaylistDescription>>();
Ok(albums)
})
}
fn add_to_playlist(&self, id: &str, uris: Vec<String>) -> BoxFuture<SpotifyResult<()>> {
let id = id.to_owned();
Box::pin(async move {
self.cache
.set_expired_pattern(&playlist_cache_key(&id))
.await
.unwrap_or(());
self.client
.add_to_playlist(&id, uris)
.send_no_response()
.await?;
Ok(())
})
}
fn create_new_playlist(
&self,
name: &str,
user_id: &str,
) -> BoxFuture<SpotifyResult<PlaylistDescription>> {
let name = name.to_owned();
let user_id = user_id.to_owned();
Box::pin(async move {
let playlist = self
.client
.create_new_playlist(&name, &user_id)
.send()
.await?
.deserialize()
.unwrap();
Ok(playlist.into())
})
}
fn remove_from_playlist(&self, id: &str, uris: Vec<String>) -> BoxFuture<SpotifyResult<()>> {
let id = id.to_owned();
Box::pin(async move {
self.cache
.set_expired_pattern(&playlist_cache_key(&id))
.await
.unwrap_or(());
self.client
.remove_from_playlist(&id, uris)
.send_no_response()
.await?;
Ok(())
})
}
fn update_playlist_details(&self, id: &str, name: String) -> BoxFuture<SpotifyResult<()>> {
let id = id.to_owned();
Box::pin(async move {
self.cache
.set_expired_pattern(&playlist_cache_key(&id))
.await
.unwrap_or(());
self.client
.update_playlist_details(&id, name)
.send_no_response()
.await?;
Ok(())
})
}
fn get_album(&self, id: &str) -> BoxFuture<SpotifyResult<AlbumFullDescription>> {
let id = id.to_owned();
Box::pin(async move {
let album = self.cache_get_or_write(SpotCacheKey::Album(&id), None, |etag| {
self.client.get_album(&id).etag(etag).send()
});
let liked = self.cache_get_or_write(
SpotCacheKey::AlbumLiked(&id),
Some(if self.client.has_token() {
CachePolicy::Revalidate
} else {
CachePolicy::IgnoreExpiry
}),
|etag| self.client.is_album_saved(&id).etag(etag).send(),
);
let (album, liked) = join!(album, liked);
let mut album: AlbumFullDescription = album?.into();
album.description.is_liked = liked?[0];
Ok(album)
})
}
fn save_album(&self, id: &str) -> BoxFuture<SpotifyResult<AlbumDescription>> {
let id = id.to_owned();
Box::pin(async move {
let _ = self.cache.set_expired_pattern(&ME_ALBUMS_CACHE).await;
self.client.save_album(&id).send_no_response().await?;
self.get_album(&id[..]).await.map(|a| a.description)
})
}
fn save_tracks(&self, ids: Vec<String>) -> BoxFuture<SpotifyResult<()>> {
Box::pin(async move {
let _ = self.cache.set_expired_pattern(&ME_TRACKS_CACHE).await;
self.client.save_tracks(ids).send_no_response().await?;
Ok(())
})
}
fn remove_saved_album(&self, id: &str) -> BoxFuture<SpotifyResult<()>> {
let id = id.to_owned();
Box::pin(async move {
let _ = self.cache.set_expired_pattern(&ME_ALBUMS_CACHE).await;
self.client.remove_saved_album(&id).send_no_response().await
})
}
fn remove_saved_tracks(&self, ids: Vec<String>) -> BoxFuture<SpotifyResult<()>> {
Box::pin(async move {
let _ = self.cache.set_expired_pattern(&ME_TRACKS_CACHE).await;
self.client
.remove_saved_tracks(ids)
.send_no_response()
.await
})
}
fn get_album_tracks(
&self,
id: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<SongBatch>> {
let id = id.to_owned();
Box::pin(async move {
let album = self.cache_get_or_write(
SpotCacheKey::Album(&id),
Some(CachePolicy::IgnoreExpiry),
|etag| self.client.get_album(&id).etag(etag).send(),
);
let songs = self.cache_get_or_write(
SpotCacheKey::AlbumTracks(&id, offset, limit),
None,
|etag| {
self.client
.get_album_tracks(&id, offset, limit)
.etag(etag)
.send()
},
);
let (album, songs) = join!(album, songs);
Ok((songs?, &album?.album).into())
})
}
fn get_playlist(&self, id: &str) -> BoxFuture<SpotifyResult<PlaylistDescription>> {
let id = id.to_owned();
Box::pin(async move {
let playlist = self
.cache_get_or_write(SpotCacheKey::Playlist(&id), None, |etag| {
self.client.get_playlist(&id).etag(etag).send()
})
.await?;
Ok(playlist.into())
})
}
fn get_playlist_tracks(
&self,
id: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<SongBatch>> {
let id = id.to_owned();
Box::pin(async move {
let songs = self
.cache_get_or_write(
SpotCacheKey::PlaylistTracks(&id, offset, limit),
None,
|etag| {
self.client
.get_playlist_tracks(&id, offset, limit)
.etag(etag)
.send()
},
)
.await?;
Ok(songs.into())
})
}
fn get_artist_albums(
&self,
id: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Vec<AlbumDescription>>> {
let id = id.to_owned();
Box::pin(async move {
let albums = self
.cache_get_or_write(
SpotCacheKey::ArtistAlbums(&id, offset, limit),
None,
|etag| {
self.client
.get_artist_albums(&id, offset, limit)
.etag(etag)
.send()
},
)
.await?;
let albums = albums
.into_iter()
.map(|a| a.into())
.collect::<Vec<AlbumDescription>>();
Ok(albums)
})
}
fn get_artist(&self, id: &str) -> BoxFuture<SpotifyResult<ArtistDescription>> {
let id = id.to_owned();
Box::pin(async move {
let artist = self.cache_get_or_write(SpotCacheKey::Artist(&id), None, |etag| {
self.client.get_artist(&id).etag(etag).send()
});
let albums = self.get_artist_albums(&id, 0, 20);
let top_tracks =
self.cache_get_or_write(SpotCacheKey::ArtistTopTracks(&id), None, |etag| {
self.client.get_artist_top_tracks(&id).etag(etag).send()
});
let (artist, albums, top_tracks) = join!(artist, albums, top_tracks);
let artist = artist?;
let result = ArtistDescription {
id: artist.id,
name: artist.name,
albums: albums?,
top_tracks: top_tracks?.into(),
};
Ok(result)
})
}
fn search(
&self,
query: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<SearchResults>> {
let query = query.to_owned();
Box::pin(async move {
let results = self
.client
.search(query, offset, limit)
.send()
.await?
.deserialize()
.ok_or(SpotifyApiError::NoContent)?;
let albums = results
.albums
.unwrap_or_default()
.into_iter()
.map(|saved| saved.into())
.collect::<Vec<AlbumDescription>>();
let artists = results
.artists
.unwrap_or_default()
.into_iter()
.map(|saved| saved.into())
.collect::<Vec<ArtistSummary>>();
Ok(SearchResults { albums, artists })
})
}
fn get_user_playlists(
&self,
id: &str,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Vec<PlaylistDescription>>> {
let id = id.to_owned();
Box::pin(async move {
let playlists = self
.cache_get_or_write(
SpotCacheKey::UserPlaylists(&id, offset, limit),
None,
|etag| {
self.client
.get_user_playlists(&id, offset, limit)
.etag(etag)
.send()
},
)
.await?;
let playlists = playlists
.into_iter()
.map(|a| a.into())
.collect::<Vec<PlaylistDescription>>();
Ok(playlists)
})
}
fn get_user(&self, id: &str) -> BoxFuture<SpotifyResult<UserDescription>> {
let id = id.to_owned();
Box::pin(async move {
let user = self.cache_get_or_write(SpotCacheKey::User(&id), None, |etag| {
self.client.get_user(&id).etag(etag).send()
});
let playlists = self.get_user_playlists(&id, 0, 30);
let (user, playlists) = join!(user, playlists);
let user = user?;
let result = UserDescription {
id: user.id,
name: user.display_name,
playlists: playlists?,
};
Ok(result)
})
}
fn list_available_devices(&self) -> BoxFuture<SpotifyResult<Vec<ConnectDevice>>> {
Box::pin(async move {
let devices = self
.client
.get_player_devices()
.send()
.await?
.deserialize()
.ok_or(SpotifyApiError::NoContent)?;
Ok(devices
.devices
.into_iter()
.filter(|d| {
debug!("found device: {:?}", d);
!d.is_restricted
})
.map(ConnectDevice::from)
.collect())
})
}
fn get_player_queue(&self) -> BoxFuture<SpotifyResult<Vec<SongDescription>>> {
Box::pin(async move {
let queue = self
.client
.get_player_queue()
.send()
.await?
.deserialize()
.ok_or(SpotifyApiError::NoContent)?;
Ok(queue.into())
})
}
fn player_pause(&self, device_id: String) -> BoxFuture<SpotifyResult<()>> {
Box::pin(self.client.player_pause(&device_id).send_no_response())
}
fn player_resume(&self, device_id: String) -> BoxFuture<SpotifyResult<()>> {
Box::pin(self.client.player_resume(&device_id).send_no_response())
}
fn player_play_in_context(
&self,
device_id: String,
context_uri: String,
offset: usize,
) -> BoxFuture<SpotifyResult<()>> {
Box::pin(
self.client
.player_set_playing(
&device_id,
PlayRequest::Contextual {
context_uri,
offset: PlayOffset {
position: offset as u32,
},
},
)
.send_no_response(),
)
}
fn player_play_no_context(
&self,
device_id: String,
uris: Vec<String>,
offset: usize,
) -> BoxFuture<SpotifyResult<()>> {
Box::pin(
self.client
.player_set_playing(
&device_id,
PlayRequest::Uris {
uris,
offset: PlayOffset {
position: offset as u32,
},
},
)
.send_no_response(),
)
}
fn player_next(&self, device_id: String) -> BoxFuture<SpotifyResult<()>> {
Box::pin(self.client.player_next(&device_id).send_no_response())
}
fn player_seek(&self, device_id: String, pos: usize) -> BoxFuture<SpotifyResult<()>> {
Box::pin(self.client.player_seek(&device_id, pos).send_no_response())
}
fn player_state(&self) -> BoxFuture<SpotifyResult<ConnectPlayerState>> {
Box::pin(async move {
let result = self
.client
.player_state()
.send()
.await?
.deserialize()
.ok_or(SpotifyApiError::NoContent)?;
Ok(result.into())
})
}
fn player_repeat(&self, device_id: String, mode: RepeatMode) -> BoxFuture<SpotifyResult<()>> {
Box::pin(
self.client
.player_repeat(
&device_id,
match mode {
RepeatMode::Song => "track",
RepeatMode::Playlist => "context",
RepeatMode::None => "off",
},
)
.send_no_response(),
)
}
fn player_shuffle(&self, device_id: String, shuffle: bool) -> BoxFuture<SpotifyResult<()>> {
Box::pin(
self.client
.player_shuffle(&device_id, shuffle)
.send_no_response(),
)
}
fn player_volume(&self, device_id: String, volume: u8) -> BoxFuture<SpotifyResult<()>> {
Box::pin(
self.client
.player_volume(&device_id, volume)
.send_no_response(),
)
}
}
#[cfg(test)]
pub mod tests {
use crate::api::api_models::*;
#[test]
fn test_search_query() {
let query = SearchQuery {
query: "test".to_string(),
types: vec![SearchType::Album, SearchType::Artist],
limit: 5,
offset: 0,
};
assert_eq!(
query.into_query_string(),
"type=album,artist&q=test&offset=0&limit=5&market=from_token"
);
}
#[test]
fn test_search_query_spaces_and_stuff() {
let query = SearchQuery {
query: "test??? wow".to_string(),
types: vec![SearchType::Album],
limit: 5,
offset: 0,
};
assert_eq!(
query.into_query_string(),
"type=album&q=test+wow&offset=0&limit=5&market=from_token"
);
}
#[test]
fn test_search_query_encoding() {
let query = SearchQuery {
query: "кириллица".to_string(),
types: vec![SearchType::Album],
limit: 5,
offset: 0,
};
assert_eq!(query.into_query_string(), "type=album&q=%D0%BA%D0%B8%D1%80%D0%B8%D0%BB%D0%BB%D0%B8%D1%86%D0%B0&offset=0&limit=5&market=from_token");
}
}

716
src/api/client.rs Normal file
View file

@ -0,0 +1,716 @@
use form_urlencoded::Serializer;
use isahc::config::Configurable;
use isahc::http::{method::Method, request::Builder, StatusCode, Uri};
use isahc::{AsyncReadResponseExt, HttpClient, Request};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use serde::{de::Deserialize, Serialize};
use serde_json::from_str;
use std::convert::Into;
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Mutex;
use thiserror::Error;
pub use super::api_models::*;
use super::cache::CacheError;
const SPOTIFY_HOST: &str = "api.spotify.com";
// https://url.spec.whatwg.org/#path-percent-encode-set
const PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'`')
.add(b'{')
.add(b'}');
fn make_query_params<'a>() -> Serializer<'a, String> {
Serializer::new(String::new())
}
pub(crate) struct SpotifyRequest<'a, Body, Response> {
client: &'a SpotifyClient,
request: Builder,
body: Body,
_type: PhantomData<Response>,
}
impl<'a, B, R> SpotifyRequest<'a, B, R>
where
B: Into<isahc::AsyncBody>,
{
fn method(mut self, method: Method) -> Self {
self.request = self.request.method(method);
self
}
fn uri(mut self, path: String, query: Option<&str>) -> Self {
let path_and_query = match query {
None => path,
Some(query) => format!("{path}?{query}"),
};
let uri = Uri::builder()
.scheme("https")
.authority(SPOTIFY_HOST)
.path_and_query(&path_and_query[..])
.build()
.unwrap();
self.request = self.request.uri(uri);
self
}
fn authenticated(mut self) -> Result<Self, SpotifyApiError> {
let token = self.client.token.lock().unwrap();
let token = token.as_ref().ok_or(SpotifyApiError::NoToken)?;
self.request = self
.request
.header("Authorization", format!("Bearer {token}"));
Ok(self)
}
pub(crate) fn etag(mut self, etag: Option<String>) -> Self {
if let Some(etag) = etag {
self.request = self.request.header("If-None-Match", etag);
}
self
}
pub(crate) fn json_body<NewBody>(self, body: NewBody) -> SpotifyRequest<'a, Vec<u8>, R>
where
NewBody: Serialize,
{
let Self {
client,
request,
_type,
..
} = self;
SpotifyRequest {
client,
request: request.header("Content-Type", "application/json"),
body: serde_json::to_vec(&body).unwrap(),
_type,
}
}
pub(crate) async fn send(self) -> Result<SpotifyResponse<R>, SpotifyApiError> {
let Self {
client,
request,
body,
..
} = self.authenticated()?;
client.send_req(request.body(body).unwrap()).await
}
pub(crate) async fn send_no_response(self) -> Result<(), SpotifyApiError> {
let Self {
client,
request,
body,
..
} = self.authenticated()?;
client
.send_req_no_response(request.body(body).unwrap())
.await
}
}
pub(crate) enum SpotifyResponseKind<T> {
Ok(String, PhantomData<T>),
NotModified,
}
pub(crate) struct SpotifyResponse<T> {
pub kind: SpotifyResponseKind<T>,
pub max_age: u64,
pub etag: Option<String>,
}
impl<'a, T> SpotifyResponse<T>
where
T: Deserialize<'a>,
{
pub(crate) fn deserialize(&'a self) -> Option<T> {
if let SpotifyResponseKind::Ok(ref content, _) = self.kind {
from_str(content)
.map_err(|e| error!("Deserialization failed: {}", e))
.ok()
} else {
None
}
}
}
#[derive(Error, Debug)]
pub enum SpotifyApiError {
#[error("Invalid token")]
InvalidToken,
#[error("No token")]
NoToken,
#[error("No content from request")]
NoContent,
#[error("Request rate exceeded")]
TooManyRequests,
#[error("Request failed ({0}): {1}")]
BadStatus(u16, String),
#[error(transparent)]
ClientError(#[from] isahc::Error),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
CacheError(#[from] CacheError),
#[error(transparent)]
ParseError(#[from] serde_json::Error),
#[error(transparent)]
ConversionError(#[from] std::string::FromUtf8Error),
}
pub(crate) struct SpotifyClient {
token: Mutex<Option<String>>,
client: HttpClient,
}
impl SpotifyClient {
pub(crate) fn new() -> Self {
let mut builder = HttpClient::builder();
if cfg!(debug_assertions) {
builder = builder.ssl_options(isahc::config::SslOption::DANGER_ACCEPT_INVALID_CERTS);
}
let client = builder.build().unwrap();
Self {
token: Mutex::new(None),
client,
}
}
pub(crate) fn request<T>(&self) -> SpotifyRequest<'_, (), T> {
SpotifyRequest {
client: self,
request: Builder::new(),
body: (),
_type: PhantomData,
}
}
pub(crate) fn has_token(&self) -> bool {
self.token.lock().unwrap().is_some()
}
pub(crate) fn update_token(&self, new_token: String) {
if let Ok(mut token) = self.token.lock() {
*token = Some(new_token)
}
}
fn clear_token(&self) {
if let Ok(mut token) = self.token.lock() {
*token = None
}
}
fn parse_cache_control(cache_control: &str) -> Option<u64> {
cache_control
.split(',')
.find(|s| s.trim().starts_with("max-age="))
.and_then(|s| s.split('=').nth(1))
.and_then(|s| u64::from_str(s).ok())
}
async fn send_req<B, T>(
&self,
request: Request<B>,
) -> Result<SpotifyResponse<T>, SpotifyApiError>
where
B: Into<isahc::AsyncBody>,
{
let mut result = self.client.send_async(request).await?;
let etag = result
.headers()
.get("etag")
.and_then(|header| header.to_str().ok())
.map(|s| s.to_owned());
let cache_control = result
.headers()
.get("cache-control")
.and_then(|header| header.to_str().ok())
.and_then(Self::parse_cache_control);
match result.status() {
StatusCode::NO_CONTENT => Err(SpotifyApiError::NoContent),
s if s.is_success() => Ok(SpotifyResponse {
kind: SpotifyResponseKind::Ok(result.text().await?, PhantomData),
max_age: cache_control.unwrap_or(10),
etag,
}),
StatusCode::UNAUTHORIZED => {
self.clear_token();
Err(SpotifyApiError::InvalidToken)
}
StatusCode::TOO_MANY_REQUESTS => Err(SpotifyApiError::TooManyRequests),
StatusCode::NOT_MODIFIED => Ok(SpotifyResponse {
kind: SpotifyResponseKind::NotModified,
max_age: cache_control.unwrap_or(10),
etag,
}),
s => Err(SpotifyApiError::BadStatus(
s.as_u16(),
result
.text()
.await
.unwrap_or_else(|_| "(no details available)".to_string()),
)),
}
}
async fn send_req_no_response<B>(&self, request: Request<B>) -> Result<(), SpotifyApiError>
where
B: Into<isahc::AsyncBody>,
{
let mut result = self.client.send_async(request).await?;
match result.status() {
StatusCode::UNAUTHORIZED => {
self.clear_token();
Err(SpotifyApiError::InvalidToken)
}
StatusCode::TOO_MANY_REQUESTS => Err(SpotifyApiError::TooManyRequests),
StatusCode::NOT_MODIFIED => Ok(()),
s if s.is_success() => Ok(()),
s => Err(SpotifyApiError::BadStatus(
s.as_u16(),
result
.text()
.await
.unwrap_or_else(|_| "(no details available)".to_string()),
)),
}
}
}
impl SpotifyClient {
pub(crate) fn get_artist(&self, id: &str) -> SpotifyRequest<'_, (), Artist> {
self.request()
.method(Method::GET)
.uri(format!("/v1/artists/{id}"), None)
}
pub(crate) fn get_artist_albums(
&self,
id: &str,
offset: usize,
limit: usize,
) -> SpotifyRequest<'_, (), Page<Album>> {
let query = make_query_params()
.append_pair("include_groups", "album,single")
.append_pair("country", "from_token")
.append_pair("offset", &offset.to_string()[..])
.append_pair("limit", &limit.to_string()[..])
.finish();
self.request()
.method(Method::GET)
.uri(format!("/v1/artists/{id}/albums"), Some(&query))
}
pub(crate) fn get_artist_top_tracks(&self, id: &str) -> SpotifyRequest<'_, (), TopTracks> {
let query = make_query_params()
.append_pair("market", "from_token")
.finish();
self.request()
.method(Method::GET)
.uri(format!("/v1/artists/{id}/top-tracks"), Some(&query))
}
pub(crate) fn is_album_saved(&self, id: &str) -> SpotifyRequest<'_, (), Vec<bool>> {
let query = make_query_params().append_pair("ids", id).finish();
self.request()
.method(Method::GET)
.uri("/v1/me/albums/contains".to_string(), Some(&query))
}
pub(crate) fn save_album(&self, id: &str) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params().append_pair("ids", id).finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/albums".to_string(), Some(&query))
}
pub(crate) fn save_tracks(&self, ids: Vec<String>) -> SpotifyRequest<'_, Vec<u8>, ()> {
self.request()
.method(Method::PUT)
.uri("/v1/me/tracks".to_string(), None)
.json_body(Ids { ids })
}
pub(crate) fn remove_saved_album(&self, id: &str) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params().append_pair("ids", id).finish();
self.request()
.method(Method::DELETE)
.uri("/v1/me/albums".to_string(), Some(&query))
}
pub(crate) fn remove_saved_tracks(&self, ids: Vec<String>) -> SpotifyRequest<'_, Vec<u8>, ()> {
self.request()
.method(Method::DELETE)
.uri("/v1/me/tracks".to_string(), None)
.json_body(Ids { ids })
}
pub(crate) fn get_album(&self, id: &str) -> SpotifyRequest<'_, (), FullAlbum> {
self.request()
.method(Method::GET)
.uri(format!("/v1/albums/{id}"), None)
}
pub(crate) fn get_album_tracks(
&self,
id: &str,
offset: usize,
limit: usize,
) -> SpotifyRequest<'_, (), Page<AlbumTrackItem>> {
let query = make_query_params()
.append_pair("offset", &offset.to_string()[..])
.append_pair("limit", &limit.to_string()[..])
.finish();
self.request()
.method(Method::GET)
.uri(format!("/v1/albums/{id}/tracks"), Some(&query))
}
pub(crate) fn get_playlist(&self, id: &str) -> SpotifyRequest<'_, (), Playlist> {
let query = make_query_params()
.append_pair(
"fields",
"id,name,images,owner,tracks(total,items(is_local,track(name,id,uri,duration_ms,artists(name,id),album(name,id,images,artists))))",
)
.finish();
self.request()
.method(Method::GET)
.uri(format!("/v1/playlists/{id}"), Some(&query))
}
pub(crate) fn get_playlist_tracks(
&self,
id: &str,
offset: usize,
limit: usize,
) -> SpotifyRequest<'_, (), Page<PlaylistTrack>> {
let query = make_query_params()
.append_pair("offset", &offset.to_string()[..])
.append_pair("limit", &limit.to_string()[..])
.finish();
self.request()
.method(Method::GET)
.uri(format!("/v1/playlists/{id}/tracks"), Some(&query))
}
pub(crate) fn add_to_playlist(
&self,
playlist: &str,
uris: Vec<String>,
) -> SpotifyRequest<'_, Vec<u8>, ()> {
self.request()
.method(Method::POST)
.uri(format!("/v1/playlists/{playlist}/tracks"), None)
.json_body(Uris { uris })
}
pub(crate) fn create_new_playlist(
&self,
name: &str,
user_id: &str,
) -> SpotifyRequest<'_, Vec<u8>, Playlist> {
self.request()
.method(Method::POST)
.uri(format!("/v1/users/{user_id}/playlists"), None)
.json_body(Name { name })
}
pub(crate) fn remove_from_playlist(
&self,
playlist: &str,
uris: Vec<String>,
) -> SpotifyRequest<'_, Vec<u8>, ()> {
self.request()
.method(Method::DELETE)
.uri(format!("/v1/playlists/{playlist}/tracks"), None)
.json_body(Uris { uris })
}
pub(crate) fn update_playlist_details(
&self,
playlist: &str,
name: String,
) -> SpotifyRequest<'_, Vec<u8>, ()> {
self.request()
.method(Method::PUT)
.uri(format!("/v1/playlists/{playlist}"), None)
.json_body(PlaylistDetails { name })
}
pub(crate) fn get_saved_albums(
&self,
offset: usize,
limit: usize,
) -> SpotifyRequest<'_, (), Page<SavedAlbum>> {
let query = make_query_params()
.append_pair("offset", &offset.to_string()[..])
.append_pair("limit", &limit.to_string()[..])
.finish();
self.request()
.method(Method::GET)
.uri("/v1/me/albums".to_string(), Some(&query))
}
pub(crate) fn get_saved_tracks(
&self,
offset: usize,
limit: usize,
) -> SpotifyRequest<'_, (), Page<SavedTrack>> {
let query = make_query_params()
.append_pair("offset", &offset.to_string()[..])
.append_pair("limit", &limit.to_string()[..])
.finish();
self.request()
.method(Method::GET)
.uri("/v1/me/tracks".to_string(), Some(&query))
}
pub(crate) fn get_saved_playlists(
&self,
offset: usize,
limit: usize,
) -> SpotifyRequest<'_, (), Page<Playlist>> {
let query = make_query_params()
.append_pair("offset", &offset.to_string()[..])
.append_pair("limit", &limit.to_string()[..])
.finish();
self.request()
.method(Method::GET)
.uri("/v1/me/playlists".to_string(), Some(&query))
}
pub(crate) fn search(
&self,
query: String,
offset: usize,
limit: usize,
) -> SpotifyRequest<'_, (), RawSearchResults> {
let query = SearchQuery {
query,
types: vec![SearchType::Album, SearchType::Artist],
limit,
offset,
};
self.request()
.method(Method::GET)
.uri("/v1/search".to_string(), Some(&query.into_query_string()))
}
pub(crate) fn get_user(&self, id: &str) -> SpotifyRequest<'_, (), User> {
let id = utf8_percent_encode(id, PATH_ENCODE_SET);
self.request()
.method(Method::GET)
.uri(format!("/v1/users/{id}"), None)
}
pub(crate) fn get_user_playlists(
&self,
id: &str,
offset: usize,
limit: usize,
) -> SpotifyRequest<'_, (), Page<Playlist>> {
let id = utf8_percent_encode(id, PATH_ENCODE_SET);
let query = make_query_params()
.append_pair("offset", &offset.to_string()[..])
.append_pair("limit", &limit.to_string()[..])
.finish();
self.request()
.method(Method::GET)
.uri(format!("/v1/users/{id}/playlists"), Some(&query))
}
pub(crate) fn get_player_devices(&self) -> SpotifyRequest<'_, (), Devices> {
self.request()
.method(Method::GET)
.uri("/v1/me/player/devices".to_string(), None)
}
pub(crate) fn get_player_queue(&self) -> SpotifyRequest<'_, (), PlayerQueue> {
self.request()
.method(Method::GET)
.uri("/v1/me/player/queue".to_string(), None)
}
pub(crate) fn player_state(&self) -> SpotifyRequest<'_, (), PlayerState> {
self.request()
.method(Method::GET)
.uri("/v1/me/player".to_string(), None)
}
pub(crate) fn player_resume(&self, device_id: &str) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params()
.append_pair("device_id", device_id)
.finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/player/play".to_string(), Some(&query))
}
pub(crate) fn player_set_playing(
&self,
device_id: &str,
request: PlayRequest,
) -> SpotifyRequest<'_, Vec<u8>, ()> {
let query = make_query_params()
.append_pair("device_id", device_id)
.finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/player/play".to_string(), Some(&query))
.json_body(request)
}
pub(crate) fn player_pause(&self, device_id: &str) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params()
.append_pair("device_id", device_id)
.finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/player/pause".to_string(), Some(&query))
}
pub(crate) fn player_next(&self, device_id: &str) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params()
.append_pair("device_id", device_id)
.finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/player/next".to_string(), Some(&query))
}
pub(crate) fn player_seek(&self, device_id: &str, pos: usize) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params()
.append_pair("device_id", device_id)
.append_pair("position_ms", &pos.to_string()[..])
.finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/player/seek".to_string(), Some(&query))
}
pub(crate) fn player_repeat(&self, device_id: &str, state: &str) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params()
.append_pair("device_id", device_id)
.append_pair("state", state)
.finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/player/repeat".to_string(), Some(&query))
}
pub(crate) fn player_shuffle(
&self,
device_id: &str,
shuffle: bool,
) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params()
.append_pair("device_id", device_id)
.append_pair("state", if shuffle { "true" } else { "false" })
.finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/player/shuffle".to_string(), Some(&query))
}
pub(crate) fn player_volume(&self, device_id: &str, volume: u8) -> SpotifyRequest<'_, (), ()> {
let query = make_query_params()
.append_pair("device_id", device_id)
.append_pair("volume_percent", &volume.to_string())
.finish();
self.request()
.method(Method::PUT)
.uri("/v1/me/player/volume".to_string(), Some(&query))
}
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn test_username_encoding() {
let username = "anna.lafuente❤";
let client = SpotifyClient::new();
let req = client.get_user(username);
assert_eq!(
req.request
.uri_ref()
.and_then(|u| u.path_and_query())
.unwrap()
.as_str(),
"/v1/users/anna.lafuente%E2%9D%A4"
);
}
#[test]
fn test_search_query() {
let query = SearchQuery {
query: "test".to_string(),
types: vec![SearchType::Album, SearchType::Artist],
limit: 5,
offset: 0,
};
assert_eq!(
query.into_query_string(),
"type=album,artist&q=test&offset=0&limit=5&market=from_token"
);
}
#[test]
fn test_search_query_spaces_and_stuff() {
let query = SearchQuery {
query: "test??? wow".to_string(),
types: vec![SearchType::Album],
limit: 5,
offset: 0,
};
assert_eq!(
query.into_query_string(),
"type=album&q=test+wow&offset=0&limit=5&market=from_token"
);
}
#[test]
fn test_search_query_encoding() {
let query = SearchQuery {
query: "кириллица".to_string(),
types: vec![SearchType::Album],
limit: 5,
offset: 0,
};
assert_eq!(query.into_query_string(), "type=album&q=%D0%BA%D0%B8%D1%80%D0%B8%D0%BB%D0%BB%D0%B8%D1%86%D0%B0&offset=0&limit=5&market=from_token");
}
}

16
src/api/mod.rs Normal file
View file

@ -0,0 +1,16 @@
mod api_models;
mod cached_client;
mod client;
pub mod cache;
pub(crate) mod oauth2;
pub use cached_client::{CachedSpotifyClient, SpotifyApiClient, SpotifyResult};
pub use client::SpotifyApiError;
pub async fn clear_user_cache() -> Option<()> {
cache::CacheManager::for_dir("spot/net")?
.clear_cache_pattern(&cached_client::USER_CACHE)
.await
.ok()
}

293
src/api/oauth2.rs Normal file
View file

@ -0,0 +1,293 @@
//! Provides a Spotify access token using the OAuth authorization code flow
//! with PKCE.
//!
//! Assuming sufficient scopes, the returned access token may be used with Spotify's
//! Web API, and/or to establish a new Session with [`librespot_core`].
//!
//! The authorization code flow is an interactive process which requires a web browser
//! to complete. The resulting code must then be provided back from the browser to this
//! library for exchange into an access token. Providing the code can be automatic via
//! a spawned http server (mimicking Spotify's client), or manually via stdin. The latter
//! is appropriate for headless systems.
use log::{error, info, trace};
use oauth2::reqwest::http_client;
use oauth2::{
basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge,
RedirectUrl, Scope, TokenResponse, TokenUrl,
};
use std::io;
use std::time::{Duration, Instant};
use std::{
io::{BufRead, BufReader, Write},
net::{SocketAddr, TcpListener},
sync::mpsc,
};
use thiserror::Error;
use url::Url;
#[derive(Debug, Error)]
pub enum OAuthError {
#[error("Unable to parse redirect URI {uri} ({e})")]
AuthCodeBadUri { uri: String, e: url::ParseError },
#[error("Auth code param not found in URI {uri}")]
AuthCodeNotFound { uri: String },
#[error("Failed to read redirect URI from stdin")]
AuthCodeStdinRead,
#[error("Failed to bind server to {addr} ({e})")]
AuthCodeListenerBind { addr: SocketAddr, e: io::Error },
#[error("Listener terminated without accepting a connection")]
AuthCodeListenerTerminated,
#[error("Failed to read redirect URI from HTTP request")]
AuthCodeListenerRead,
#[error("Failed to parse redirect URI from HTTP request")]
AuthCodeListenerParse,
#[error("Failed to write HTTP response")]
AuthCodeListenerWrite,
#[error("Invalid Spotify OAuth URI")]
InvalidSpotifyUri,
#[error("Invalid Redirect URI {uri} ({e})")]
InvalidRedirectUri { uri: String, e: url::ParseError },
#[error("Failed to receive code")]
Recv,
#[error("Failed to exchange code for access token ({e})")]
ExchangeCode { e: String },
}
#[derive(Debug)]
pub struct OAuthToken {
pub access_token: String,
#[allow(dead_code)]
pub refresh_token: String,
pub expires_at: Instant,
#[allow(dead_code)]
pub token_type: String,
#[allow(dead_code)]
pub scopes: Vec<String>,
}
/// Return code query-string parameter from the redirect URI.
fn get_code(redirect_url: &str) -> Result<AuthorizationCode, OAuthError> {
let url = Url::parse(redirect_url).map_err(|e| OAuthError::AuthCodeBadUri {
uri: redirect_url.to_string(),
e,
})?;
let code = url
.query_pairs()
.find(|(key, _)| key == "code")
.map(|(_, code)| AuthorizationCode::new(code.into_owned()))
.ok_or(OAuthError::AuthCodeNotFound {
uri: redirect_url.to_string(),
})?;
Ok(code)
}
/// Prompt for redirect URI on stdin and return auth code.
fn get_authcode_stdin() -> Result<AuthorizationCode, OAuthError> {
println!("Provide redirect URL");
let mut buffer = String::new();
let stdin = io::stdin();
stdin
.read_line(&mut buffer)
.map_err(|_| OAuthError::AuthCodeStdinRead)?;
get_code(buffer.trim())
}
/// Spawn HTTP server at provided socket address to accept OAuth callback and return auth code.
fn get_authcode_listener(socket_address: SocketAddr) -> Result<AuthorizationCode, OAuthError> {
let listener =
TcpListener::bind(socket_address).map_err(|e| OAuthError::AuthCodeListenerBind {
addr: socket_address,
e,
})?;
info!("OAuth server listening on {:?}", socket_address);
// The server will terminate itself after collecting the first code.
let mut stream = listener
.incoming()
.flatten()
.next()
.ok_or(OAuthError::AuthCodeListenerTerminated)?;
let mut reader = BufReader::new(&stream);
let mut request_line = String::new();
reader
.read_line(&mut request_line)
.map_err(|_| OAuthError::AuthCodeListenerRead)?;
let redirect_url = request_line
.split_whitespace()
.nth(1)
.ok_or(OAuthError::AuthCodeListenerParse)?;
let code = get_code(&("http://localhost".to_string() + redirect_url));
let message = "Go back to your terminal :)";
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
message.len(),
message
);
stream
.write_all(response.as_bytes())
.map_err(|_| OAuthError::AuthCodeListenerWrite)?;
code
}
// If the specified `redirect_uri` is HTTP, loopback, and contains a port,
// then the corresponding socket address is returned.
fn get_socket_address(redirect_uri: &str) -> Option<SocketAddr> {
let url = match Url::parse(redirect_uri) {
Ok(u) if u.scheme() == "http" && u.port().is_some() => u,
_ => return None,
};
let socket_addr = match url.socket_addrs(|| None) {
Ok(mut addrs) => addrs.pop(),
_ => None,
};
if let Some(s) = socket_addr {
if s.ip().is_loopback() {
return socket_addr;
}
}
None
}
/// Obtain a Spotify access token using the authorization code with PKCE OAuth flow.
/// The redirect_uri must match what is registered to the client ID.
pub fn get_access_token(
client_id: &str,
redirect_uri: &str,
scopes: Vec<&str>,
) -> Result<OAuthToken, OAuthError> {
let auth_url = AuthUrl::new("https://accounts.spotify.com/authorize".to_string())
.map_err(|_| OAuthError::InvalidSpotifyUri)?;
let token_url = TokenUrl::new("https://accounts.spotify.com/api/token".to_string())
.map_err(|_| OAuthError::InvalidSpotifyUri)?;
let redirect_url =
RedirectUrl::new(redirect_uri.to_string()).map_err(|e| OAuthError::InvalidRedirectUri {
uri: redirect_uri.to_string(),
e,
})?;
let client = BasicClient::new(
ClientId::new(client_id.to_string()),
None,
auth_url,
Some(token_url),
)
.set_redirect_uri(redirect_url);
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
// Generate the full authorization URL.
// Some of these scopes are unavailable for custom client IDs. Which?
let request_scopes: Vec<oauth2::Scope> = scopes
.clone()
.into_iter()
.map(|s| Scope::new(s.into()))
.collect();
let (auth_url, _) = client
.authorize_url(CsrfToken::new_random)
.add_scopes(request_scopes)
.set_pkce_challenge(pkce_challenge)
.url();
println!("Browse to: {}", auth_url);
if let Err(err) = open::that(auth_url.to_string()) {
eprintln!("An error occurred when opening '{}': {}", auth_url, err)
}
let code = match get_socket_address(redirect_uri) {
Some(addr) => get_authcode_listener(addr),
_ => get_authcode_stdin(),
}?;
trace!("Exchange {code:?} for access token");
// Do this sync in another thread because I am too stupid to make the async version work.
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let resp = client
.exchange_code(code)
.set_pkce_verifier(pkce_verifier)
.request(http_client);
if let Err(e) = tx.send(resp) {
error!("OAuth channel send error: {e}");
}
});
let token_response = rx.recv().map_err(|_| OAuthError::Recv)?;
let token = token_response.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?;
trace!("Obtained new access token: {token:?}");
let token_scopes: Vec<String> = match token.scopes() {
Some(s) => s.iter().map(|s| s.to_string()).collect(),
_ => scopes.into_iter().map(|s| s.to_string()).collect(),
};
let refresh_token = match token.refresh_token() {
Some(t) => t.secret().to_string(),
_ => "".to_string(), // Spotify always provides a refresh token.
};
Ok(OAuthToken {
access_token: token.access_token().secret().to_string(),
refresh_token,
expires_at: Instant::now()
+ token
.expires_in()
.unwrap_or_else(|| Duration::from_secs(3600)),
token_type: format!("{:?}", token.token_type()).to_string(), // Urgh!?
scopes: token_scopes,
})
}
#[cfg(test)]
mod test {
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use super::*;
#[test]
fn get_socket_address_none() {
// No port
assert_eq!(get_socket_address("http://127.0.0.1/foo"), None);
assert_eq!(get_socket_address("http://127.0.0.1:/foo"), None);
assert_eq!(get_socket_address("http://[::1]/foo"), None);
// Not localhost
assert_eq!(get_socket_address("http://56.0.0.1:1234/foo"), None);
assert_eq!(
get_socket_address("http://[3ffe:2a00:100:7031::1]:1234/foo"),
None
);
// Not http
assert_eq!(get_socket_address("https://127.0.0.1/foo"), None);
}
#[test]
fn get_socket_address_localhost() {
let localhost_v4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1234);
let localhost_v6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8888);
assert_eq!(
get_socket_address("http://127.0.0.1:1234/foo"),
Some(localhost_v4)
);
assert_eq!(
get_socket_address("http://[0:0:0:0:0:0:0:1]:8888/foo"),
Some(localhost_v6)
);
assert_eq!(
get_socket_address("http://[::1]:8888/foo"),
Some(localhost_v6)
);
}
}

15
src/app.css Normal file
View file

@ -0,0 +1,15 @@
@define-color accent_bg_color @green_5;
@define-color accent_color @green_3;
.container {
transition: opacity 0.3s ease;
opacity: 0;
}
.container--loaded {
opacity: 1;
}
.playlist__title-entry--ro {
background: none;
}

105
src/app/batch_loader.rs Normal file
View file

@ -0,0 +1,105 @@
use gettextrs::gettext;
use std::sync::Arc;
use crate::api::{SpotifyApiClient, SpotifyApiError};
use crate::app::models::*;
use crate::app::AppAction;
// A wrapper around the Spotify API to load batches of songs from various sources (see below)
#[derive(Clone)]
pub struct BatchLoader {
api: Arc<dyn SpotifyApiClient + Send + Sync>,
}
// The sources mentionned above
#[derive(Clone, Debug)]
pub enum SongsSource {
Playlist(String),
Album(String),
SavedTracks,
}
impl PartialEq for SongsSource {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Playlist(l), Self::Playlist(r)) => l == r,
(Self::Album(l), Self::Album(r)) => l == r,
(Self::SavedTracks, Self::SavedTracks) => true,
_ => false,
}
}
}
impl Eq for SongsSource {}
impl SongsSource {
pub fn has_spotify_uri(&self) -> bool {
matches!(self, Self::Playlist(_) | Self::Album(_))
}
pub fn spotify_uri(&self) -> Option<String> {
match self {
Self::Playlist(id) => Some(format!("spotify:playlist:{}", id)),
Self::Album(id) => Some(format!("spotify:album:{}", id)),
_ => None,
}
}
}
// How to query for a batch: specify a source, and a batch to get (offset + number of elements to get)
#[derive(Debug)]
pub struct BatchQuery {
pub source: SongsSource,
pub batch: Batch,
}
impl BatchQuery {
// Given a query, compute the next batch to get (if any)
pub fn next(&self) -> Option<Self> {
let Self { source, batch } = self;
Some(Self {
source: source.clone(),
batch: batch.next()?,
})
}
}
impl BatchLoader {
pub fn new(api: Arc<dyn SpotifyApiClient + Send + Sync>) -> Self {
Self { api }
}
// Query a batch and create an action when it's been retrieved succesfully
pub async fn query<ActionCreator>(
&self,
query: BatchQuery,
create_action: ActionCreator,
) -> Option<AppAction>
where
ActionCreator: FnOnce(SongsSource, SongBatch) -> AppAction,
{
let api = Arc::clone(&self.api);
let Batch {
offset, batch_size, ..
} = query.batch;
let result = match &query.source {
SongsSource::Playlist(id) => api.get_playlist_tracks(id, offset, batch_size).await,
SongsSource::SavedTracks => api.get_saved_tracks(offset, batch_size).await,
SongsSource::Album(id) => api.get_album_tracks(id, offset, batch_size).await,
};
match result {
Ok(batch) => Some(create_action(query.source, batch)),
// No token? Why was the batch loader called? Ah, whatever
Err(SpotifyApiError::NoToken) => None,
Err(err) => {
error!("Spotify API error: {}", err);
Some(AppAction::ShowNotification(gettext(
// translators: This notification is the default message for unhandled errors. Logs refer to console output.
"An error occured. Check logs for details!",
)))
}
}
}
}

View file

@ -0,0 +1,75 @@
using Gtk 4.0;
using Adw 1;
template $AlbumWidget : Adw.Bin {
Button cover_btn {
hexpand: false;
halign: center;
Box {
halign: center;
valign: start;
margin-top: 6;
margin-bottom: 6;
orientation: vertical;
spacing: 6;
Image cover_image {
icon-name: "media-playback-start-symbolic";
styles [
"card",
]
}
Label album_label {
label: "Album";
justify: center;
wrap: true;
wrap-mode: word;
ellipsize: end;
max-width-chars: 1;
margin-top: 6;
styles [
"title-4",
]
}
Label artist_label {
label: "Artist";
justify: center;
wrap: true;
wrap-mode: word;
ellipsize: end;
max-width-chars: 1;
styles [
"body",
]
}
Label year_label {
label: "Year";
justify: center;
wrap: true;
wrap-mode: word_char;
max-width-chars: 1;
sensitive: false;
styles [
"body",
]
}
}
styles [
"flat",
]
}
styles [
"container",
"album",
]
}

View file

@ -0,0 +1,34 @@
/* large style */
leaflet.unfolded .album .card {
min-width: 200px;
min-height: 200px;
border-radius: 6px;
}
leaflet.unfolded .album {
margin-top: 6px;
margin-bottom: 6px;
}
leaflet.unfolded .album button {
border-radius: 12px;
}
/* small style */
leaflet.folded .album .card {
min-width: 100px;
min-height: 100px;
border-radius: 6px;
margin-top: 0px;
margin-bottom: 0px;
}
leaflet.unfolded .album {
margin-top: 0px;
margin-bottom: 0px;
}
leaflet.folded .album button {
border-radius: 6px;
}

View file

@ -0,0 +1,129 @@
use crate::app::components::display_add_css_provider;
use crate::app::dispatch::Worker;
use crate::app::loader::ImageLoader;
use crate::app::models::AlbumModel;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use libadwaita::subclass::prelude::BinImpl;
mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/dev/alextren/Spot/components/album.ui")]
pub struct AlbumWidget {
#[template_child]
pub album_label: TemplateChild<gtk::Label>,
#[template_child]
pub artist_label: TemplateChild<gtk::Label>,
#[template_child]
pub year_label: TemplateChild<gtk::Label>,
#[template_child]
pub cover_btn: TemplateChild<gtk::Button>,
#[template_child]
pub cover_image: TemplateChild<gtk::Image>,
}
#[glib::object_subclass]
impl ObjectSubclass for AlbumWidget {
const NAME: &'static str = "AlbumWidget";
type Type = super::AlbumWidget;
type ParentType = libadwaita::Bin;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for AlbumWidget {}
impl WidgetImpl for AlbumWidget {}
impl BinImpl for AlbumWidget {}
}
glib::wrapper! {
pub struct AlbumWidget(ObjectSubclass<imp::AlbumWidget>) @extends gtk::Widget, libadwaita::Bin;
}
impl Default for AlbumWidget {
fn default() -> Self {
Self::new()
}
}
impl AlbumWidget {
pub fn new() -> Self {
display_add_css_provider(resource!("/components/album.css"));
glib::Object::new()
}
pub fn for_model(album_model: &AlbumModel, worker: Worker) -> Self {
let _self = Self::new();
_self.bind(album_model, worker);
_self
}
fn set_loaded(&self) {
self.add_css_class("container--loaded");
}
fn set_image(&self, pixbuf: Option<&gdk_pixbuf::Pixbuf>) {
self.imp().cover_image.set_from_pixbuf(pixbuf);
}
fn bind(&self, album_model: &AlbumModel, worker: Worker) {
let widget = self.imp();
widget.cover_image.set_overflow(gtk::Overflow::Hidden);
if let Some(cover_art) = album_model.cover() {
let _self = self.downgrade();
worker.send_local_task(async move {
if let Some(_self) = _self.upgrade() {
let loader = ImageLoader::new();
let result = loader.load_remote(&cover_art, "jpg", 200, 200).await;
_self.set_image(result.as_ref());
_self.set_loaded();
}
});
} else {
self.set_loaded();
}
album_model
.bind_property("album", &*widget.album_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
album_model
.bind_property("artist", &*widget.artist_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
if album_model.year() > 0 {
album_model
.bind_property("year", &*widget.year_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
} else {
widget.year_label.set_visible(false);
}
}
pub fn connect_album_pressed<F: Fn(&Self) + 'static>(&self, f: F) {
self.imp()
.cover_btn
.connect_clicked(clone!(@weak self as _self => move |_| {
f(&_self);
}));
}
}

View file

@ -0,0 +1,3 @@
#[allow(clippy::module_inception)]
mod album;
pub use album::AlbumWidget;

View file

@ -0,0 +1,31 @@
using Gtk 4.0;
using Adw 1;
template $ArtistWidget : Box {
orientation: vertical;
Button avatar_btn {
vexpand: true;
width-request: 150;
height-request: 150;
receives-default: true;
halign: center;
valign: center;
has-frame: false;
Adw.Avatar avatar {
halign: center;
valign: center;
show-initials: true;
size: 150;
}
styles [
"circular",
]
}
Label artist {
label: "Artist Name";
}
}

View file

@ -0,0 +1,95 @@
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use crate::app::loader::ImageLoader;
use crate::app::models::ArtistModel;
use crate::app::Worker;
mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/dev/alextren/Spot/components/artist.ui")]
pub struct ArtistWidget {
#[template_child]
pub artist: TemplateChild<gtk::Label>,
#[template_child]
pub avatar_btn: TemplateChild<gtk::Button>,
#[template_child]
pub avatar: TemplateChild<libadwaita::Avatar>,
}
#[glib::object_subclass]
impl ObjectSubclass for ArtistWidget {
const NAME: &'static str = "ArtistWidget";
type Type = super::ArtistWidget;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ArtistWidget {}
impl WidgetImpl for ArtistWidget {}
impl BoxImpl for ArtistWidget {}
}
glib::wrapper! {
pub struct ArtistWidget(ObjectSubclass<imp::ArtistWidget>) @extends gtk::Widget, gtk::Box;
}
impl Default for ArtistWidget {
fn default() -> Self {
Self::new()
}
}
impl ArtistWidget {
pub fn new() -> Self {
glib::Object::new()
}
pub fn for_model(model: &ArtistModel, worker: Worker) -> Self {
let _self = Self::new();
_self.bind(model, worker);
_self
}
pub fn connect_artist_pressed<F: Fn(&Self) + 'static>(&self, f: F) {
self.imp()
.avatar_btn
.connect_clicked(clone!(@weak self as _self => move |_| {
f(&_self);
}));
}
fn bind(&self, model: &ArtistModel, worker: Worker) {
let widget = self.imp();
if let Some(url) = model.image() {
let avatar = widget.avatar.downgrade();
worker.send_local_task(async move {
if let Some(avatar) = avatar.upgrade() {
let loader = ImageLoader::new();
let pixbuf = loader.load_remote(&url, "jpg", 200, 200).await;
let texture = pixbuf.as_ref().map(gdk::Texture::for_pixbuf);
avatar.set_custom_image(texture.as_ref());
}
});
}
model
.bind_property("artist", &*widget.artist, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
}
}

View file

@ -0,0 +1,63 @@
using Gtk 4.0;
template $ArtistDetailsWidget : Box {
ScrolledWindow scrolled_window {
hscrollbar-policy: never;
hexpand: true;
vexpand: true;
Box {
margin-start: 8;
margin-end: 8;
margin-top: 8;
margin-bottom: 8;
orientation: vertical;
spacing: 16;
Box {
orientation: vertical;
Label {
halign: start;
margin-start: 8;
margin-end: 8;
/* Translators: Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. */
label: _("Top tracks");
styles [
"title-4",
]
}
ListView top_tracks {
}
}
Expander {
margin-top: 8;
margin-bottom: 8;
expanded: true;
FlowBox artist_releases {
height-request: 100;
hexpand: true;
min-children-per-line: 1;
selection-mode: none;
activate-on-single-click: false;
}
[label]
Label {
/* Translators: Title of the sections that contains all releases from an artist (both singles and albums). */
label: _("Releases");
}
}
}
}
styles [
"artist",
]
}

View file

@ -0,0 +1,18 @@
listview.artist__top-tracks {
padding: 8px;
border-radius: 8px;
margin: 8px;
}
listview.artist__top-tracks row {
border-radius: 4px;
}
.artist {
transition: opacity .3s ease;
opacity: 0;
}
.artist__loaded {
opacity: 1;
}

View file

@ -0,0 +1,168 @@
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use std::rc::Rc;
use crate::app::components::{
display_add_css_provider, AlbumWidget, Component, EventListener, Playlist,
};
use crate::app::{models::*, ListStore};
use crate::app::{AppEvent, BrowserEvent, Worker};
use super::ArtistDetailsModel;
mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/dev/alextren/Spot/components/artist_details.ui")]
pub struct ArtistDetailsWidget {
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub top_tracks: TemplateChild<gtk::ListView>,
#[template_child]
pub artist_releases: TemplateChild<gtk::FlowBox>,
}
#[glib::object_subclass]
impl ObjectSubclass for ArtistDetailsWidget {
const NAME: &'static str = "ArtistDetailsWidget";
type Type = super::ArtistDetailsWidget;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ArtistDetailsWidget {}
impl WidgetImpl for ArtistDetailsWidget {}
impl BoxImpl for ArtistDetailsWidget {}
}
glib::wrapper! {
pub struct ArtistDetailsWidget(ObjectSubclass<imp::ArtistDetailsWidget>) @extends gtk::Widget, gtk::Box;
}
impl ArtistDetailsWidget {
fn new() -> Self {
display_add_css_provider(resource!("/components/artist_details.css"));
glib::Object::new()
}
fn top_tracks_widget(&self) -> &gtk::ListView {
self.imp().top_tracks.as_ref()
}
fn set_loaded(&self) {
self.add_css_class("artist__loaded");
}
fn connect_bottom_edge<F>(&self, f: F)
where
F: Fn() + 'static,
{
self.imp()
.scrolled_window
.connect_edge_reached(move |_, pos| {
if let gtk::PositionType::Bottom = pos {
f()
}
});
}
fn bind_artist_releases<F>(
&self,
worker: Worker,
store: &ListStore<AlbumModel>,
on_album_pressed: F,
) where
F: Fn(String) + Clone + 'static,
{
self.imp()
.artist_releases
.bind_model(Some(store.unsafe_store()), move |item| {
let item = item.downcast_ref::<AlbumModel>().unwrap();
let child = gtk::FlowBoxChild::new();
let album = AlbumWidget::for_model(item, worker.clone());
let f = on_album_pressed.clone();
album.connect_album_pressed(clone!(@weak item => move |_| {
f(item.uri());
}));
child.set_child(Some(&album));
child.upcast::<gtk::Widget>()
});
}
}
pub struct ArtistDetails {
model: Rc<ArtistDetailsModel>,
widget: ArtistDetailsWidget,
children: Vec<Box<dyn EventListener>>,
}
impl ArtistDetails {
pub fn new(model: Rc<ArtistDetailsModel>, worker: Worker) -> Self {
model.load_artist_details(model.id.clone());
let widget = ArtistDetailsWidget::new();
widget.connect_bottom_edge(clone!(@weak model => move || {
model.load_more();
}));
if let Some(store) = model.get_list_store() {
widget.bind_artist_releases(
worker.clone(),
&store,
clone!(@weak model => move |id| {
model.open_album(id);
}),
);
}
let playlist = Box::new(Playlist::new(
widget.top_tracks_widget().clone(),
Rc::clone(&model),
worker,
));
Self {
model,
widget,
children: vec![playlist],
}
}
}
impl Component for ArtistDetails {
fn get_root_widget(&self) -> &gtk::Widget {
self.widget.upcast_ref()
}
fn get_children(&mut self) -> Option<&mut Vec<Box<dyn EventListener>>> {
Some(&mut self.children)
}
}
impl EventListener for ArtistDetails {
fn on_event(&mut self, event: &AppEvent) {
match event {
AppEvent::BrowserEvent(BrowserEvent::ArtistDetailsUpdated(id))
if id == &self.model.id =>
{
self.widget.set_loaded();
}
_ => {}
}
self.broadcast_event(event);
}
}

View file

@ -0,0 +1,187 @@
use gio::prelude::*;
use gio::SimpleActionGroup;
use std::ops::Deref;
use std::rc::Rc;
use crate::api::SpotifyApiError;
use crate::app::components::SimpleHeaderBarModel;
use crate::app::components::{labels, PlaylistModel};
use crate::app::models::*;
use crate::app::state::SelectionContext;
use crate::app::state::{
BrowserAction, BrowserEvent, PlaybackAction, SelectionAction, SelectionState,
};
use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel, ListStore};
pub struct ArtistDetailsModel {
pub id: String,
app_model: Rc<AppModel>,
dispatcher: Box<dyn ActionDispatcher>,
}
impl ArtistDetailsModel {
pub fn new(id: String, app_model: Rc<AppModel>, dispatcher: Box<dyn ActionDispatcher>) -> Self {
Self {
id,
app_model,
dispatcher,
}
}
pub fn get_artist_name(&self) -> Option<impl Deref<Target = String> + '_> {
self.app_model
.map_state_opt(|s| s.browser.artist_state(&self.id)?.artist.as_ref())
}
pub fn get_list_store(&self) -> Option<impl Deref<Target = ListStore<AlbumModel>> + '_> {
self.app_model
.map_state_opt(|s| Some(&s.browser.artist_state(&self.id)?.albums))
}
pub fn load_artist_details(&self, id: String) {
let api = self.app_model.get_spotify();
self.dispatcher
.call_spotify_and_dispatch(move || async move {
let artist = api.get_artist(&id).await;
match artist {
Ok(artist) => Ok(BrowserAction::SetArtistDetails(Box::new(artist)).into()),
Err(SpotifyApiError::BadStatus(400, _))
| Err(SpotifyApiError::BadStatus(404, _)) => {
Ok(BrowserAction::NavigationPop.into())
}
Err(e) => Err(e),
}
});
}
pub fn open_album(&self, id: String) {
self.dispatcher.dispatch(AppAction::ViewAlbum(id));
}
pub fn load_more(&self) -> Option<()> {
let api = self.app_model.get_spotify();
let state = self.app_model.get_state();
let next_page = &state.browser.artist_state(&self.id)?.next_page;
let id = next_page.data.clone();
let batch_size = next_page.batch_size;
let offset = next_page.next_offset?;
self.dispatcher
.call_spotify_and_dispatch(move || async move {
api.get_artist_albums(&id, offset, batch_size)
.await
.map(|albums| BrowserAction::AppendArtistReleases(id, albums).into())
});
Some(())
}
}
impl PlaylistModel for ArtistDetailsModel {
fn song_list_model(&self) -> SongListModel {
self.app_model
.get_state()
.browser
.artist_state(&self.id)
.expect("illegal attempt to read artist_state")
.top_tracks
.clone()
}
fn is_paused(&self) -> bool {
!self.app_model.get_state().playback.is_playing()
}
fn current_song_id(&self) -> Option<String> {
self.app_model.get_state().playback.current_song_id()
}
fn play_song_at(&self, _pos: usize, id: &str) {
let tracks: Vec<SongDescription> = self.song_list_model().collect();
self.dispatcher
.dispatch(PlaybackAction::LoadSongs(tracks).into());
self.dispatcher
.dispatch(PlaybackAction::Load(id.to_string()).into());
}
fn actions_for(&self, id: &str) -> Option<gio::ActionGroup> {
let song = self.song_list_model().get(id)?;
let song = song.description();
let group = SimpleActionGroup::new();
for view_artist in song.make_artist_actions(self.dispatcher.box_clone(), None) {
group.add_action(&view_artist);
}
group.add_action(&song.make_album_action(self.dispatcher.box_clone(), None));
group.add_action(&song.make_link_action(None));
group.add_action(&song.make_queue_action(self.dispatcher.box_clone(), None));
Some(group.upcast())
}
fn menu_for(&self, id: &str) -> Option<gio::MenuModel> {
let song = self.song_list_model().get(id)?;
let song = song.description();
let menu = gio::Menu::new();
menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album"));
for artist in song.artists.iter().filter(|a| self.id != a.id) {
menu.append(
Some(&labels::more_from_label(&artist.name)),
Some(&format!("song.view_artist_{}", artist.id)),
);
}
menu.append(Some(&*labels::COPY_LINK), Some("song.copy_link"));
menu.append(Some(&*labels::ADD_TO_QUEUE), Some("song.queue"));
Some(menu.upcast())
}
fn select_song(&self, id: &str) {
let song = self.song_list_model().get(id);
if let Some(song) = song {
self.dispatcher
.dispatch(SelectionAction::Select(vec![song.into_description()]).into());
}
}
fn deselect_song(&self, id: &str) {
self.dispatcher
.dispatch(SelectionAction::Deselect(vec![id.to_string()]).into());
}
fn enable_selection(&self) -> bool {
self.dispatcher
.dispatch(AppAction::EnableSelection(SelectionContext::Default));
true
}
fn selection(&self) -> Option<Box<dyn Deref<Target = SelectionState> + '_>> {
Some(Box::new(self.app_model.map_state(|s| &s.selection)))
}
}
impl SimpleHeaderBarModel for ArtistDetailsModel {
fn title(&self) -> Option<String> {
Some(self.get_artist_name()?.clone())
}
fn title_updated(&self, event: &AppEvent) -> bool {
matches!(
event,
AppEvent::BrowserEvent(BrowserEvent::ArtistDetailsUpdated(_))
)
}
fn selection_context(&self) -> Option<SelectionContext> {
Some(SelectionContext::Default)
}
fn select_all(&self) {
let songs: Vec<SongDescription> = self.song_list_model().collect();
self.dispatcher
.dispatch(SelectionAction::Select(songs).into());
}
}

View file

@ -0,0 +1,6 @@
#[allow(clippy::module_inception)]
mod artist_details;
pub use artist_details::*;
mod artist_details_model;
pub use artist_details_model::*;

View file

@ -0,0 +1,136 @@
using Gtk 4.0;
template $AlbumHeaderWidget : Box {
valign: start;
vexpand: false;
margin-start: 6;
margin-end: 6;
margin-bottom: 6;
Overlay album_overlay {
overflow: hidden;
halign: center;
margin-top: 18;
margin-bottom: 6;
margin-start: 6;
Image album_art {
width-request: 160;
height-request: 160;
icon-name: "emblem-music-symbolic";
}
[overlay]
Button info_button {
icon-name: "preferences-system-details-symbolic";
halign: end;
valign: end;
margin-start: 6;
margin-end: 6;
margin-top: 6;
margin-bottom: 6;
tooltip-text: "Album Info";
styles [
"circular",
"osd",
]
}
styles [
"card",
]
}
Box album_info {
hexpand: true;
valign: center;
orientation: vertical;
spacing: 6;
margin-start: 18;
Label album_label {
xalign: 0;
halign: start;
label: "Album";
wrap: true;
ellipsize: end;
max-width-chars: 50;
lines: 4;
styles [
"title-1",
]
}
LinkButton artist_button {
receives-default: true;
halign: start;
valign: center;
has-frame: false;
Label artist_button_label {
hexpand: true;
vexpand: true;
label: "Artist";
ellipsize: middle;
}
styles [
"title-4",
]
}
Label year_label {
xalign: 0;
halign: start;
label: "Year";
ellipsize: end;
max-width-chars: 50;
lines: 1;
sensitive: false;
styles [
"body",
]
}
}
Box button_box {
orientation: horizontal;
valign: center;
margin-end: 6;
spacing: 8;
Button play_button {
receives-default: true;
halign: center;
valign: center;
tooltip-text: "Play";
icon-name: "media-playback-start-symbolic";
styles [
"circular",
"play__button",
]
}
Button like_button {
receives-default: true;
halign: center;
valign: center;
tooltip-text: "Add to Library";
styles [
"circular",
"like__button",
]
}
}
styles [
"album__header",
]
}

View file

@ -0,0 +1,33 @@
.album__header .title-4 label {
color: @window_fg_color;
font-weight: bold;
text-decoration: none;
}
.album__header .title-4:hover {
border-radius: 6px;
background-image: image(alpha(currentColor, 0.08));
}
clamp.details__clamp {
background-color: @view_bg_color;
box-shadow: inset 0px -1px 0px @borders;
}
headerbar.details__headerbar {
transition: background-color .3s ease;
}
headerbar.flat.details__headerbar windowtitle {
opacity: 0;
}
headerbar.details__headerbar windowtitle {
transition: opacity .3s ease;
opacity: 1;
}
.details__headerbar.flat {
background-color: @view_bg_color;
}

View file

@ -0,0 +1,166 @@
use crate::app::components::display_add_css_provider;
use gettextrs::gettext;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate};
mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/dev/alextren/Spot/components/album_header.ui")]
pub struct AlbumHeaderWidget {
#[template_child]
pub album_overlay: TemplateChild<gtk::Overlay>,
#[template_child]
pub album_label: TemplateChild<gtk::Label>,
#[template_child]
pub album_art: TemplateChild<gtk::Image>,
#[template_child]
pub button_box: TemplateChild<gtk::Box>,
#[template_child]
pub like_button: TemplateChild<gtk::Button>,
#[template_child]
pub play_button: TemplateChild<gtk::Button>,
#[template_child]
pub info_button: TemplateChild<gtk::Button>,
#[template_child]
pub album_info: TemplateChild<gtk::Box>,
#[template_child]
pub artist_button: TemplateChild<gtk::LinkButton>,
#[template_child]
pub artist_button_label: TemplateChild<gtk::Label>,
#[template_child]
pub year_label: TemplateChild<gtk::Label>,
}
#[glib::object_subclass]
impl ObjectSubclass for AlbumHeaderWidget {
const NAME: &'static str = "AlbumHeaderWidget";
type Type = super::AlbumHeaderWidget;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
display_add_css_provider(resource!("/components/album_header.css"));
obj.init_template();
}
}
impl ObjectImpl for AlbumHeaderWidget {}
impl WidgetImpl for AlbumHeaderWidget {}
impl BoxImpl for AlbumHeaderWidget {}
}
glib::wrapper! {
pub struct AlbumHeaderWidget(ObjectSubclass<imp::AlbumHeaderWidget>) @extends gtk::Widget, gtk::Box;
}
impl Default for AlbumHeaderWidget {
fn default() -> Self {
Self::new()
}
}
impl AlbumHeaderWidget {
pub fn new() -> Self {
glib::Object::new()
}
pub fn connect_play<F>(&self, f: F)
where
F: Fn() + 'static,
{
self.imp().play_button.connect_clicked(move |_| f());
}
pub fn connect_liked<F>(&self, f: F)
where
F: Fn() + 'static,
{
self.imp().like_button.connect_clicked(move |_| f());
}
pub fn connect_info<F>(&self, f: F)
where
F: Fn() + 'static,
{
self.imp().info_button.connect_clicked(move |_| f());
}
pub fn connect_artist_clicked<F>(&self, f: F)
where
F: Fn() + 'static,
{
self.imp().artist_button.connect_activate_link(move |_| {
f();
glib::signal::Inhibit(true)
});
}
pub fn set_liked(&self, is_liked: bool) {
self.imp().like_button.set_icon_name(if is_liked {
"starred-symbolic"
} else {
"non-starred-symbolic"
});
}
pub fn set_playing(&self, is_playing: bool) {
let playback_icon = if is_playing {
"media-playback-pause-symbolic"
} else {
"media-playback-start-symbolic"
};
let translated_tooltip = if is_playing {
gettext("Pause")
} else {
gettext("Play")
};
let tooltip_text = Some(translated_tooltip.as_str());
self.imp().play_button.set_icon_name(playback_icon);
self.imp().play_button.set_tooltip_text(tooltip_text);
}
pub fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) {
self.imp().album_art.set_from_pixbuf(Some(art));
}
pub fn set_album_and_artist_and_year(&self, album: &str, artist: &str, year: Option<u32>) {
let widget = self.imp();
widget.album_label.set_label(album);
widget.artist_button_label.set_label(artist);
match year {
Some(year) => widget.year_label.set_label(&year.to_string()),
None => widget.year_label.set_visible(false),
}
}
pub fn set_centered(&self) {
let widget = self.imp();
widget.album_label.set_halign(gtk::Align::Center);
widget.album_label.set_justify(gtk::Justification::Center);
widget.artist_button.set_halign(gtk::Align::Center);
widget.year_label.set_halign(gtk::Align::Center);
widget.button_box.set_halign(gtk::Align::Center);
widget.album_overlay.set_margin_start(0);
widget.button_box.set_margin_end(0);
widget.album_info.set_margin_start(0);
}
}

View file

@ -0,0 +1,55 @@
using Gtk 4.0;
using Adw 1;
template $AlbumDetailsWidget : Adw.Bin {
Box {
orientation: vertical;
vexpand: true;
hexpand: true;
$HeaderBarWidget headerbar {
}
$ScrollingHeaderWidget scrolling_header {
[header]
WindowHandle {
Adw.Clamp {
maximum-size: 900;
Adw.Squeezer {
switch-threshold-policy: natural;
valign: center;
homogeneous: false;
transition-type: crossfade;
$AlbumHeaderWidget header_widget {
}
$AlbumHeaderWidget header_mobile {
orientation: "vertical";
spacing: "12";
}
}
styles [
"details__clamp",
]
}
}
Adw.ClampScrollable {
maximum-size: 900;
ListView album_tracks {
styles [
"album__tracks",
]
}
}
styles [
"container",
]
}
}
}

View file

@ -0,0 +1,335 @@
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use std::rc::Rc;
use super::album_header::AlbumHeaderWidget;
use super::release_details::ReleaseDetailsWindow;
use super::DetailsModel;
use crate::app::components::{
Component, EventListener, HeaderBarComponent, HeaderBarWidget, Playlist, ScrollingHeaderWidget,
};
use crate::app::dispatch::Worker;
use crate::app::loader::ImageLoader;
use crate::app::state::PlaybackEvent;
use crate::app::{AppEvent, BrowserEvent};
mod imp {
use libadwaita::subclass::prelude::BinImpl;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/dev/alextren/Spot/components/details.ui")]
pub struct AlbumDetailsWidget {
#[template_child]
pub scrolling_header: TemplateChild<ScrollingHeaderWidget>,
#[template_child]
pub headerbar: TemplateChild<HeaderBarWidget>,
#[template_child]
pub header_widget: TemplateChild<AlbumHeaderWidget>,
#[template_child]
pub header_mobile: TemplateChild<AlbumHeaderWidget>,
#[template_child]
pub album_tracks: TemplateChild<gtk::ListView>,
}
#[glib::object_subclass]
impl ObjectSubclass for AlbumDetailsWidget {
const NAME: &'static str = "AlbumDetailsWidget";
type Type = super::AlbumDetailsWidget;
type ParentType = libadwaita::Bin;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for AlbumDetailsWidget {
fn constructed(&self) {
self.parent_constructed();
self.header_mobile.set_centered();
self.headerbar.add_classes(&["details__headerbar"]);
}
}
impl WidgetImpl for AlbumDetailsWidget {}
impl BinImpl for AlbumDetailsWidget {}
}
glib::wrapper! {
pub struct AlbumDetailsWidget(ObjectSubclass<imp::AlbumDetailsWidget>) @extends gtk::Widget, libadwaita::Bin;
}
impl AlbumDetailsWidget {
fn new() -> Self {
glib::Object::new()
}
fn set_header_visible(&self, visible: bool) {
let widget = self.imp();
widget.headerbar.set_title_visible(true);
if visible {
widget.headerbar.add_classes(&["flat"]);
} else {
widget.headerbar.remove_classes(&["flat"]);
}
}
fn connect_header(&self) {
self.set_header_visible(false);
self.imp().scrolling_header.connect_header_visibility(
clone!(@weak self as _self => move |visible| {
_self.set_header_visible(visible);
}),
);
}
fn connect_bottom_edge<F>(&self, f: F)
where
F: Fn() + 'static,
{
self.imp().scrolling_header.connect_bottom_edge(f);
}
fn headerbar_widget(&self) -> &HeaderBarWidget {
self.imp().headerbar.as_ref()
}
fn album_tracks_widget(&self) -> &gtk::ListView {
self.imp().album_tracks.as_ref()
}
fn set_loaded(&self) {
self.imp()
.scrolling_header
.add_css_class("container--loaded");
}
fn connect_liked<F>(&self, f: F)
where
F: Fn() + Clone + 'static,
{
self.imp().header_widget.connect_liked(f.clone());
self.imp().header_mobile.connect_liked(f);
}
fn connect_play<F>(&self, f: F)
where
F: Fn() + Clone + 'static,
{
self.imp().header_widget.connect_play(f.clone());
self.imp().header_mobile.connect_play(f);
}
fn connect_info<F>(&self, f: F)
where
F: Fn() + Clone + 'static,
{
self.imp().header_widget.connect_info(f.clone());
self.imp().header_mobile.connect_info(f);
}
fn set_liked(&self, is_liked: bool) {
self.imp().header_widget.set_liked(is_liked);
self.imp().header_mobile.set_liked(is_liked);
}
fn set_playing(&self, is_playing: bool) {
self.imp().header_widget.set_playing(is_playing);
self.imp().header_mobile.set_playing(is_playing);
}
fn set_album_and_artist_and_year(&self, album: &str, artist: &str, year: Option<u32>) {
self.imp()
.header_widget
.set_album_and_artist_and_year(album, artist, year);
self.imp()
.header_mobile
.set_album_and_artist_and_year(album, artist, year);
self.imp().headerbar.set_title_and_subtitle(album, artist);
}
fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) {
self.imp().header_widget.set_artwork(art);
self.imp().header_mobile.set_artwork(art);
}
fn connect_artist_clicked<F>(&self, f: F)
where
F: Fn() + Clone + 'static,
{
self.imp().header_widget.connect_artist_clicked(f.clone());
self.imp().header_mobile.connect_artist_clicked(f);
}
}
pub struct Details {
model: Rc<DetailsModel>,
worker: Worker,
widget: AlbumDetailsWidget,
modal: ReleaseDetailsWindow,
children: Vec<Box<dyn EventListener>>,
}
impl Details {
pub fn new(model: Rc<DetailsModel>, worker: Worker, leaflet: &libadwaita::Leaflet) -> Self {
if model.get_album_info().is_none() {
model.load_album_info();
}
let widget = AlbumDetailsWidget::new();
let playlist = Box::new(Playlist::new(
widget.album_tracks_widget().clone(),
model.clone(),
worker.clone(),
));
let headerbar_widget = widget.headerbar_widget();
headerbar_widget.bind_to_leaflet(leaflet);
let headerbar = Box::new(HeaderBarComponent::new(
headerbar_widget.clone(),
model.to_headerbar_model(),
));
let modal = ReleaseDetailsWindow::new();
widget.connect_liked(clone!(@weak model => move || model.toggle_save_album()));
widget.connect_play(clone!(@weak model => move || model.toggle_play_album()));
widget.connect_header();
widget.connect_bottom_edge(clone!(@weak model => move || {
model.load_more();
}));
widget.connect_info(clone!(@weak modal, @weak widget => move || {
let modal = modal.upcast_ref::<libadwaita::Window>();
modal.set_modal(true);
modal.set_transient_for(
widget
.root()
.and_then(|r| r.downcast::<gtk::Window>().ok())
.as_ref(),
);
modal.set_visible(true);
}));
Self {
model,
worker,
widget,
modal,
children: vec![playlist, headerbar],
}
}
fn update_liked(&self) {
if let Some(info) = self.model.get_album_info() {
let is_liked = info.description.is_liked;
self.widget.set_liked(is_liked);
self.widget.set_liked(is_liked);
}
}
fn update_playing(&self, is_playing: bool) {
if !self.model.album_is_playing() || !self.model.is_playing() {
self.widget.set_playing(false);
return;
}
self.widget.set_playing(is_playing);
}
fn update_details(&mut self) {
if let Some(album) = self.model.get_album_info() {
let details = &album.release_details;
let album = &album.description;
self.widget.set_liked(album.is_liked);
self.widget.set_album_and_artist_and_year(
&album.title[..],
&album.artists_name(),
album.year(),
);
self.widget.connect_artist_clicked(
clone!(@weak self.model as model => move || model.view_artist()),
);
self.modal.set_details(
&album.title,
&album.artists_name(),
&details.label,
album.release_date.as_ref().unwrap(),
details.total_tracks,
&details.copyright_text,
);
if let Some(art) = album.art.clone() {
let widget = self.widget.downgrade();
self.worker.send_local_task(async move {
let pixbuf = ImageLoader::new()
.load_remote(&art[..], "jpg", 320, 320)
.await;
if let (Some(widget), Some(ref pixbuf)) = (widget.upgrade(), pixbuf) {
widget.set_artwork(pixbuf);
widget.set_loaded();
}
});
} else {
self.widget.set_loaded();
}
}
}
}
impl Component for Details {
fn get_root_widget(&self) -> &gtk::Widget {
self.widget.upcast_ref()
}
fn get_children(&mut self) -> Option<&mut Vec<Box<dyn EventListener>>> {
Some(&mut self.children)
}
}
impl EventListener for Details {
fn on_event(&mut self, event: &AppEvent) {
match event {
AppEvent::BrowserEvent(BrowserEvent::AlbumDetailsLoaded(id))
if id == &self.model.id =>
{
self.update_details();
self.update_playing(true);
}
AppEvent::BrowserEvent(BrowserEvent::AlbumSaved(id))
| AppEvent::BrowserEvent(BrowserEvent::AlbumUnsaved(id))
if id == &self.model.id =>
{
self.update_liked();
}
AppEvent::PlaybackEvent(PlaybackEvent::PlaybackPaused) => {
self.update_playing(false);
}
AppEvent::PlaybackEvent(PlaybackEvent::PlaybackResumed) => {
self.update_playing(true);
}
_ => {}
}
self.broadcast_event(event);
}
}

View file

@ -0,0 +1,272 @@
use gio::prelude::*;
use gio::SimpleActionGroup;
use std::cell::Ref;
use std::ops::Deref;
use std::rc::Rc;
use crate::api::SpotifyApiError;
use crate::app::components::labels;
use crate::app::components::HeaderBarModel;
use crate::app::components::PlaylistModel;
use crate::app::components::SimpleHeaderBarModel;
use crate::app::components::SimpleHeaderBarModelWrapper;
use crate::app::dispatch::ActionDispatcher;
use crate::app::models::*;
use crate::app::state::SelectionContext;
use crate::app::state::{BrowserAction, PlaybackAction, SelectionAction, SelectionState};
use crate::app::{AppAction, AppEvent, AppModel, AppState, BatchQuery, SongsSource};
pub struct DetailsModel {
pub id: String,
app_model: Rc<AppModel>,
dispatcher: Box<dyn ActionDispatcher>,
}
impl DetailsModel {
pub fn new(id: String, app_model: Rc<AppModel>, dispatcher: Box<dyn ActionDispatcher>) -> Self {
Self {
id,
app_model,
dispatcher,
}
}
fn state(&self) -> Ref<'_, AppState> {
self.app_model.get_state()
}
pub fn get_album_info(&self) -> Option<impl Deref<Target = AlbumFullDescription> + '_> {
self.app_model
.map_state_opt(|s| s.browser.details_state(&self.id)?.content.as_ref())
}
pub fn get_album_description(&self) -> Option<impl Deref<Target = AlbumDescription> + '_> {
self.app_model.map_state_opt(|s| {
Some(
&s.browser
.details_state(&self.id)?
.content
.as_ref()?
.description,
)
})
}
pub fn load_album_info(&self) {
let id = self.id.clone();
let api = self.app_model.get_spotify();
self.dispatcher
.call_spotify_and_dispatch(move || async move {
let album = api.get_album(&id).await;
match album {
Ok(album) => Ok(BrowserAction::SetAlbumDetails(Box::new(album)).into()),
Err(SpotifyApiError::BadStatus(400, _))
| Err(SpotifyApiError::BadStatus(404, _)) => {
Ok(BrowserAction::NavigationPop.into())
}
Err(e) => Err(e),
}
});
}
pub fn view_artist(&self) {
if let Some(album) = self.get_album_description() {
let artist = &album.artists.first().unwrap().id;
self.dispatcher
.dispatch(AppAction::ViewArtist(artist.to_owned()));
}
}
pub fn toggle_save_album(&self) {
if let Some(album) = self.get_album_description() {
let id = album.id.clone();
let is_liked = album.is_liked;
let api = self.app_model.get_spotify();
self.dispatcher
.call_spotify_and_dispatch(move || async move {
if !is_liked {
api.save_album(&id)
.await
.map(|album| BrowserAction::SaveAlbum(Box::new(album)).into())
} else {
api.remove_saved_album(&id)
.await
.map(|_| BrowserAction::UnsaveAlbum(id).into())
}
});
}
}
pub fn is_playing(&self) -> bool {
self.state().playback.is_playing()
}
pub fn album_is_playing(&self) -> bool {
matches!(
self.app_model.get_state().playback.current_source(),
Some(SongsSource::Album(ref id)) if id == &self.id)
}
pub fn toggle_play_album(&self) {
if let Some(album) = self.get_album_description() {
if !self.album_is_playing() {
if self.state().playback.is_shuffled() {
self.dispatcher
.dispatch(AppAction::PlaybackAction(PlaybackAction::ToggleShuffle));
}
let id_of_first_song = album.songs.songs[0].id.as_str();
self.play_song_at(0, id_of_first_song);
return;
}
if self.state().playback.is_playing() {
self.dispatcher
.dispatch(AppAction::PlaybackAction(PlaybackAction::Pause));
} else {
self.dispatcher
.dispatch(AppAction::PlaybackAction(PlaybackAction::Play));
}
}
}
pub fn load_more(&self) -> Option<()> {
let last_batch = self.song_list_model().last_batch()?;
let query = BatchQuery {
source: SongsSource::Album(self.id.clone()),
batch: last_batch,
};
let id = self.id.clone();
let next_query = query.next()?;
let loader = self.app_model.get_batch_loader();
self.dispatcher.dispatch_async(Box::pin(async move {
loader
.query(next_query, |_s, song_batch| {
BrowserAction::AppendAlbumTracks(id, Box::new(song_batch)).into()
})
.await
}));
Some(())
}
pub fn to_headerbar_model(self: &Rc<Self>) -> Rc<impl HeaderBarModel> {
Rc::new(SimpleHeaderBarModelWrapper::new(
self.clone(),
self.app_model.clone(),
self.dispatcher.box_clone(),
))
}
}
impl PlaylistModel for DetailsModel {
fn song_list_model(&self) -> SongListModel {
self.app_model
.get_state()
.browser
.details_state(&self.id)
.expect("illegal attempt to read details_state")
.songs
.clone()
}
fn is_paused(&self) -> bool {
!self.app_model.get_state().playback.is_playing()
}
fn show_song_covers(&self) -> bool {
false
}
fn select_song(&self, id: &str) {
let songs = self.song_list_model();
if let Some(song) = songs.get(id) {
self.dispatcher
.dispatch(SelectionAction::Select(vec![song.description().clone()]).into());
}
}
fn deselect_song(&self, id: &str) {
self.dispatcher
.dispatch(SelectionAction::Deselect(vec![id.to_string()]).into());
}
fn enable_selection(&self) -> bool {
self.dispatcher
.dispatch(AppAction::EnableSelection(SelectionContext::Default));
true
}
fn selection(&self) -> Option<Box<dyn Deref<Target = SelectionState> + '_>> {
Some(Box::new(self.app_model.map_state(|s| &s.selection)))
}
fn current_song_id(&self) -> Option<String> {
self.state().playback.current_song_id()
}
fn play_song_at(&self, pos: usize, id: &str) {
let source = SongsSource::Album(self.id.clone());
let batch = self.song_list_model().song_batch_for(pos);
if let Some(batch) = batch {
self.dispatcher
.dispatch(PlaybackAction::LoadPagedSongs(source, batch).into());
self.dispatcher
.dispatch(PlaybackAction::Load(id.to_string()).into());
}
}
fn actions_for(&self, id: &str) -> Option<gio::ActionGroup> {
let song = self.song_list_model().get(id)?;
let song = song.description();
let group = SimpleActionGroup::new();
for view_artist in song.make_artist_actions(self.dispatcher.box_clone(), None) {
group.add_action(&view_artist);
}
group.add_action(&song.make_link_action(None));
group.add_action(&song.make_queue_action(self.dispatcher.box_clone(), None));
Some(group.upcast())
}
fn menu_for(&self, id: &str) -> Option<gio::MenuModel> {
let song = self.song_list_model().get(id)?;
let song = song.description();
let menu = gio::Menu::new();
for artist in song.artists.iter() {
menu.append(
Some(&labels::more_from_label(&artist.name)),
Some(&format!("song.view_artist_{}", artist.id)),
);
}
menu.append(Some(&*labels::COPY_LINK), Some("song.copy_link"));
menu.append(Some(&*labels::ADD_TO_QUEUE), Some("song.queue"));
Some(menu.upcast())
}
}
impl SimpleHeaderBarModel for DetailsModel {
fn title(&self) -> Option<String> {
None
}
fn title_updated(&self, _: &AppEvent) -> bool {
false
}
fn selection_context(&self) -> Option<SelectionContext> {
Some(SelectionContext::Default)
}
fn select_all(&self) {
let songs: Vec<SongDescription> = self.song_list_model().collect();
self.dispatcher
.dispatch(SelectionAction::Select(songs).into());
}
}

View file

@ -0,0 +1,8 @@
mod album_header;
#[allow(clippy::module_inception)]
mod details;
mod details_model;
mod release_details;
pub use details::Details;
pub use details_model::DetailsModel;

View file

@ -0,0 +1,82 @@
using Gtk 4.0;
using Adw 1;
template $ReleaseDetailsWindow : Adw.Window {
modal: true;
hide-on-close: true;
default-width: 360;
Box {
orientation: vertical;
Adw.HeaderBar {
show-end-title-buttons: true;
[title]
Adw.WindowTitle album_artist {
}
styles [
"flat",
]
}
ListBox {
margin-start: 6;
margin-end: 6;
margin-top: 6;
margin-bottom: 6;
valign: start;
selection-mode: none;
show-separators: true;
overflow: hidden;
styles [
"card",
]
Adw.ActionRow {
/* Translators: This refers to a music label */
title: _("Label");
[suffix]
Label label {
label: "Label";
}
}
Adw.ActionRow {
/* Translators: This refers to a release date */
title: _("Released");
[suffix]
Label release {
label: "Released";
}
}
Adw.ActionRow {
/* Translators: This refers to a number of tracks */
title: _("Tracks");
[suffix]
Label tracks {
label: "Tracks";
}
}
Adw.ActionRow {
title: _("Copyright");
[suffix]
Label copyright {
label: "Copyright";
ellipsize: middle;
}
}
}
}
}

View file

@ -0,0 +1,92 @@
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use libadwaita::subclass::prelude::*;
use crate::app::components::labels;
mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/dev/alextren/Spot/components/release_details.ui")]
pub struct ReleaseDetailsWindow {
#[template_child]
pub album_artist: TemplateChild<libadwaita::WindowTitle>,
#[template_child]
pub label: TemplateChild<gtk::Label>,
#[template_child]
pub release: TemplateChild<gtk::Label>,
#[template_child]
pub tracks: TemplateChild<gtk::Label>,
#[template_child]
pub copyright: TemplateChild<gtk::Label>,
}
#[glib::object_subclass]
impl ObjectSubclass for ReleaseDetailsWindow {
const NAME: &'static str = "ReleaseDetailsWindow";
type Type = super::ReleaseDetailsWindow;
type ParentType = libadwaita::Window;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ReleaseDetailsWindow {
fn constructed(&self) {
self.parent_constructed();
}
}
impl WidgetImpl for ReleaseDetailsWindow {}
impl AdwWindowImpl for ReleaseDetailsWindow {}
impl WindowImpl for ReleaseDetailsWindow {}
}
glib::wrapper! {
pub struct ReleaseDetailsWindow(ObjectSubclass<imp::ReleaseDetailsWindow>) @extends gtk::Widget, libadwaita::Window, libadwaita::PreferencesWindow;
}
impl Default for ReleaseDetailsWindow {
fn default() -> Self {
Self::new()
}
}
impl ReleaseDetailsWindow {
pub fn new() -> Self {
glib::Object::new()
}
#[allow(clippy::too_many_arguments)]
pub fn set_details(
&self,
album: &str,
artist: &str,
label: &str,
release_date: &str,
track_count: usize,
copyright: &str,
) {
let widget = self.imp();
widget
.album_artist
.set_title(&labels::album_by_artist_label(album, artist));
widget.label.set_text(label);
widget.release.set_text(release_date);
widget.tracks.set_text(&track_count.to_string());
widget.copyright.set_text(copyright);
}
}

Some files were not shown because too many files have changed in this diff Show more