Adding a bunch of flake8 extensions and working through the errors
This commit is contained in:
16
Pipfile
16
Pipfile
@@ -4,21 +4,25 @@ url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
docutils = "*"
|
||||
flake8 = "*"
|
||||
flake8-annotations = "*"
|
||||
flake8-comprehensions = "*"
|
||||
flake8-pep3101 = "*"
|
||||
flake8-print = "*"
|
||||
graphviz = "*"
|
||||
jedi = "*"
|
||||
lxml = "*"
|
||||
mypy = "*"
|
||||
yapf = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
docutils = "*"
|
||||
lxml = "*"
|
||||
jedi = "*"
|
||||
rope = "*"
|
||||
rst2html5 = "*"
|
||||
graphviz = "*"
|
||||
sphinx = "*"
|
||||
sphinx-rtd-theme = "*"
|
||||
sphinx-autodoc-typehints = "*"
|
||||
sphinx-rtd-theme = "*"
|
||||
termcolor = "*"
|
||||
yapf = "*"
|
||||
|
||||
[packages]
|
||||
sublime-music = {editable = true,path = "."}
|
||||
|
45
Pipfile.lock
generated
45
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "c16627b97b66d2ad7016bd43428e26f5f29836ba28eb797d27c4cc80f8a70a99"
|
||||
"sha256": "1697c1d3c4480dbec759d96e80b367e508d813cb2183a1c6226f56ee0be8fac6"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -383,6 +383,37 @@
|
||||
"index": "pypi",
|
||||
"version": "==3.7.9"
|
||||
},
|
||||
"flake8-annotations": {
|
||||
"hashes": [
|
||||
"sha256:19a6637a5da1bb7ea7948483ca9e2b9e15b213e687e7bf5ff8c1bfc91c185006",
|
||||
"sha256:bb033b72cdd3a2b0a530bbdf2081f12fbea7d70baeaaebb5899723a45f424b8e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"flake8-comprehensions": {
|
||||
"hashes": [
|
||||
"sha256:d08323aa801aef33477cd33f2f5ce3acb1aafd26803ab0d171d85d514c1273a2",
|
||||
"sha256:e7db586bb6eb95afdfd87ed244c90e57ae1352db8ef0ad3012fca0200421e5df"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.2"
|
||||
},
|
||||
"flake8-pep3101": {
|
||||
"hashes": [
|
||||
"sha256:86e3eb4e42de8326dcd98ebdeaf9a3c6854203a48f34aeb3e7e8ed948107f512",
|
||||
"sha256:a5dae1caca1243b2b40108dce926d97cf5a9f52515c4a4cbb1ffe1ca0c54e343"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"flake8-print": {
|
||||
"hashes": [
|
||||
"sha256:324f9e59a522518daa2461bacd7f82da3c34eb26a4314c2a54bd493f8b394a68"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.4"
|
||||
},
|
||||
"genshi": {
|
||||
"hashes": [
|
||||
"sha256:5e92e278ca1ea395349a451d54fc81dc3c1b543c48939a15bd36b7b3335e1560",
|
||||
@@ -659,11 +690,11 @@
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:525527074f2e0c2585f68f73c99b4dc257c34bbe308b27f5f8c7a6e20642742f",
|
||||
"sha256:543d39db5f82d83a5c1aa0c10c88f2b6cff2da3e711aa849b2c627b4b403bbd9"
|
||||
"sha256:776ff8333181138fae52df65be733127539623bb46cc692e7fa0fcfc80d7aa88",
|
||||
"sha256:ca762da97c3b5107cbf0ab9e11d3ec7ab8d3c31377266fd613b962ed971df709"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.4.2"
|
||||
"version": "==2.4.3"
|
||||
},
|
||||
"sphinx-autodoc-typehints": {
|
||||
"hashes": [
|
||||
@@ -697,10 +728,10 @@
|
||||
},
|
||||
"sphinxcontrib-htmlhelp": {
|
||||
"hashes": [
|
||||
"sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
|
||||
"sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
|
||||
"sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f",
|
||||
"sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"
|
||||
],
|
||||
"version": "==1.0.2"
|
||||
"version": "==1.0.3"
|
||||
},
|
||||
"sphinxcontrib-jsmath": {
|
||||
"hashes": [
|
||||
|
@@ -1,14 +1,16 @@
|
||||
#! /usr/bin/env python3
|
||||
"""
|
||||
Autogenerates Python classes for Subsonic API objects.
|
||||
|
||||
This program constructs a dependency graph of all of the entities defined by a
|
||||
Subsonic REST API XSD file. It then uses that graph to generate code which
|
||||
represents those API objects in Python.
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from typing import cast, Dict, DefaultDict, Set, Match, Tuple, List
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import DefaultDict, Dict, List, Set, Tuple
|
||||
|
||||
from graphviz import Digraph
|
||||
from lxml import etree
|
||||
@@ -25,12 +27,13 @@ primitive_translation_map = {
|
||||
}
|
||||
|
||||
|
||||
def render_digraph(graph, filename):
|
||||
def render_digraph(graph: DefaultDict[str, Set[str]], filename: str):
|
||||
"""
|
||||
Renders a graph of form {'node_name': iterable(node_name)} to ``filename``.
|
||||
Render a graph of the form {'node_name': iterable(node_name)} to
|
||||
``filename``.
|
||||
"""
|
||||
g = Digraph('G', filename=f'/tmp/{filename}', format='png')
|
||||
for type_, deps in dependency_graph.items():
|
||||
for type_, deps in graph.items():
|
||||
g.node(type_)
|
||||
|
||||
for dep in deps:
|
||||
@@ -39,21 +42,27 @@ def render_digraph(graph, filename):
|
||||
g.render()
|
||||
|
||||
|
||||
def primitive_translate(type_str):
|
||||
def primitive_translate(type_str: str) -> str:
|
||||
# Translate the primitive values, but default to the actual value.
|
||||
return primitive_translation_map.get(type_str, type_str)
|
||||
|
||||
|
||||
def extract_type(type_str):
|
||||
return primitive_translate(
|
||||
cast(Match, element_type_re.match(type_str)).group(1))
|
||||
def extract_type(type_str: str) -> str:
|
||||
match = element_type_re.match(type_str)
|
||||
if not match:
|
||||
raise Exception(f'Could not extract type from string "{type_str}"')
|
||||
return primitive_translate(match.group(1))
|
||||
|
||||
|
||||
def extract_tag_type(tag_type_str):
|
||||
return cast(Match, tag_type_re.match(tag_type_str)).group(1)
|
||||
def extract_tag_type(tag_type_str: str) -> str:
|
||||
match = tag_type_re.match(tag_type_str)
|
||||
if not match:
|
||||
raise Exception(
|
||||
f'Could not extract tag type from string "{tag_type_str}"')
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def get_dependencies(xs_el) -> Tuple[Set[str], Dict[str, str]]:
|
||||
def get_dependencies(xs_el: etree._Element) -> Tuple[Set[str], Dict[str, str]]:
|
||||
"""
|
||||
Return the types which ``xs_el`` depends on as well as the type of the
|
||||
object for embedding in other objects.
|
||||
@@ -162,7 +171,7 @@ def get_dependencies(xs_el) -> Tuple[Set[str], Dict[str, str]]:
|
||||
# Check arguments.
|
||||
# =============================================================================
|
||||
if len(sys.argv) < 3:
|
||||
print(f'Usage: {sys.argv[0]} <schema_file> <output_file>.')
|
||||
print(f'Usage: {sys.argv[0]} <schema_file> <output_file>.') # noqa: T001
|
||||
sys.exit(1)
|
||||
|
||||
schema_file, output_file = sys.argv[1:]
|
||||
@@ -201,7 +210,7 @@ seen: Set[str] = set()
|
||||
i = 0
|
||||
|
||||
|
||||
def dfs(g, el):
|
||||
def dfs(g: DefaultDict[str, Set[str]], el: str):
|
||||
global i
|
||||
if el in seen:
|
||||
return
|
||||
@@ -224,7 +233,7 @@ output_order.remove('subsonic-response')
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def generate_class_for_type(type_name):
|
||||
def generate_class_for_type(type_name: str) -> str:
|
||||
fields = type_fields[type_name]
|
||||
|
||||
code = ['', '']
|
||||
@@ -262,12 +271,12 @@ def generate_class_for_type(type_name):
|
||||
# Auto-generated __eq__ and __hash__ functions if there's an ID field.
|
||||
if not is_enum and has_properties and 'id' in fields:
|
||||
code.append('')
|
||||
code.append(indent_str.format('def __eq__(self, other):'))
|
||||
code.append(indent_str.format('def __eq__(self, other: Any) -> bool:'))
|
||||
code.append(indent_str.format(' return hash(self) == hash(other)'))
|
||||
|
||||
hash_name = inherits or type_name
|
||||
code.append('')
|
||||
code.append(indent_str.format('def __hash__(self):'))
|
||||
code.append(indent_str.format('def __hash__(self) -> int:'))
|
||||
code.append(
|
||||
indent_str.format(f" return hash(f'{hash_name}.{{self.id}}')"))
|
||||
|
||||
@@ -286,8 +295,9 @@ with open(output_file, 'w+') as outfile:
|
||||
'"""',
|
||||
'',
|
||||
'from datetime import datetime',
|
||||
'from typing import List',
|
||||
'from enum import Enum',
|
||||
'from typing import Any, List',
|
||||
'',
|
||||
'from sublime.server.api_object import APIObject',
|
||||
*map(generate_class_for_type, output_order),
|
||||
]) + '\n')
|
||||
|
638
api_object_generator/api_specs/subsonic-rest-api-1.16.0.xsd
Normal file
638
api_object_generator/api_specs/subsonic-rest-api-1.16.0.xsd
Normal file
@@ -0,0 +1,638 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:sub="http://subsonic.org/restapi"
|
||||
targetNamespace="http://subsonic.org/restapi"
|
||||
attributeFormDefault="unqualified"
|
||||
elementFormDefault="qualified"
|
||||
version="1.16.0">
|
||||
|
||||
<xs:element name="subsonic-response" type="sub:Response"/>
|
||||
|
||||
<xs:complexType name="Response">
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="musicFolders" type="sub:MusicFolders" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="indexes" type="sub:Indexes" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="directory" type="sub:Directory" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="genres" type="sub:Genres" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="artists" type="sub:ArtistsID3" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="artist" type="sub:ArtistWithAlbumsID3" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="album" type="sub:AlbumWithSongsID3" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="videos" type="sub:Videos" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="videoInfo" type="sub:VideoInfo" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="nowPlaying" type="sub:NowPlaying" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="searchResult" type="sub:SearchResult" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="searchResult2" type="sub:SearchResult2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="searchResult3" type="sub:SearchResult3" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="playlists" type="sub:Playlists" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="playlist" type="sub:PlaylistWithSongs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="jukeboxStatus" type="sub:JukeboxStatus" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="jukeboxPlaylist" type="sub:JukeboxPlaylist" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="license" type="sub:License" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="users" type="sub:Users" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="user" type="sub:User" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="chatMessages" type="sub:ChatMessages" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="albumList" type="sub:AlbumList" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="albumList2" type="sub:AlbumList2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="randomSongs" type="sub:Songs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="songsByGenre" type="sub:Songs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="lyrics" type="sub:Lyrics" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="podcasts" type="sub:Podcasts" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="newestPodcasts" type="sub:NewestPodcasts" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="internetRadioStations" type="sub:InternetRadioStations" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="bookmarks" type="sub:Bookmarks" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="playQueue" type="sub:PlayQueue" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="shares" type="sub:Shares" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="starred" type="sub:Starred" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="starred2" type="sub:Starred2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="albumInfo" type="sub:AlbumInfo" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="artistInfo" type="sub:ArtistInfo" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="artistInfo2" type="sub:ArtistInfo2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="similarSongs" type="sub:SimilarSongs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="similarSongs2" type="sub:SimilarSongs2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="topSongs" type="sub:TopSongs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="scanStatus" type="sub:ScanStatus" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="error" type="sub:Error" minOccurs="1" maxOccurs="1"/>
|
||||
</xs:choice>
|
||||
<xs:attribute name="status" type="sub:ResponseStatus" use="required"/>
|
||||
<xs:attribute name="version" type="sub:Version" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="ResponseStatus">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="ok"/>
|
||||
<xs:enumeration value="failed"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="Version">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="\d+\.\d+\.\d+"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="MusicFolders">
|
||||
<xs:sequence>
|
||||
<xs:element name="musicFolder" type="sub:MusicFolder" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="MusicFolder">
|
||||
<xs:attribute name="id" type="xs:int" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Indexes">
|
||||
<xs:sequence>
|
||||
<xs:element name="shortcut" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="index" type="sub:Index" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="child" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/> <!-- Added in 1.7.0 -->
|
||||
</xs:sequence>
|
||||
<xs:attribute name="lastModified" type="xs:long" use="required"/>
|
||||
<xs:attribute name="ignoredArticles" type="xs:string" use="required"/> <!-- Added in 1.10.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Index">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Artist">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.10.1 -->
|
||||
<xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Genres">
|
||||
<xs:sequence>
|
||||
<xs:element name="genre" type="sub:Genre" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Genre" mixed="true">
|
||||
<xs:attribute name="songCount" type="xs:int" use="required"/> <!-- Added in 1.10.2 -->
|
||||
<xs:attribute name="albumCount" type="xs:int" use="required"/> <!-- Added in 1.10.2 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistsID3">
|
||||
<xs:sequence>
|
||||
<xs:element name="index" type="sub:IndexID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="ignoredArticles" type="xs:string" use="required"/> <!-- Added in 1.10.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="IndexID3">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistID3">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="albumCount" type="xs:int" use="required"/>
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistWithAlbumsID3">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:ArtistID3">
|
||||
<xs:sequence>
|
||||
<xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumID3">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="artist" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="artistId" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="songCount" type="xs:int" use="required"/>
|
||||
<xs:attribute name="duration" type="xs:int" use="required"/>
|
||||
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
|
||||
<xs:attribute name="created" type="xs:dateTime" use="required"/>
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/>
|
||||
<xs:attribute name="year" type="xs:int" use="optional"/> <!-- Added in 1.10.1 -->
|
||||
<xs:attribute name="genre" type="xs:string" use="optional"/> <!-- Added in 1.10.1 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumWithSongsID3">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:AlbumID3">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Videos">
|
||||
<xs:sequence>
|
||||
<xs:element name="video" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="VideoInfo">
|
||||
<xs:sequence>
|
||||
<xs:element name="captions" type="sub:Captions" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="audioTrack" type="sub:AudioTrack" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="conversion" type="sub:VideoConversion" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Captions">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AudioTrack">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="languageCode" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="VideoConversion">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="bitRate" type="xs:int" use="optional"/> <!-- In Kbps -->
|
||||
<xs:attribute name="audioTrackId" type="xs:int" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Directory">
|
||||
<xs:sequence>
|
||||
<xs:element name="child" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="parent" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.10.1 -->
|
||||
<xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Child">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="parent" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="isDir" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="title" type="xs:string" use="required"/>
|
||||
<xs:attribute name="album" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="artist" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="track" type="xs:int" use="optional"/>
|
||||
<xs:attribute name="year" type="xs:int" use="optional"/>
|
||||
<xs:attribute name="genre" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="size" type="xs:long" use="optional"/>
|
||||
<xs:attribute name="contentType" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="suffix" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="transcodedContentType" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="transcodedSuffix" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="duration" type="xs:int" use="optional"/>
|
||||
<xs:attribute name="bitRate" type="xs:int" use="optional"/>
|
||||
<xs:attribute name="path" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="isVideo" type="xs:boolean" use="optional"/> <!-- Added in 1.4.1 -->
|
||||
<xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.6.0 -->
|
||||
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.6.0 -->
|
||||
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
|
||||
<xs:attribute name="discNumber" type="xs:int" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="created" type="xs:dateTime" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="albumId" type="xs:string" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="artistId" type="xs:string" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="type" type="sub:MediaType" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="bookmarkPosition" type="xs:long" use="optional"/> <!-- In millis. Added in 1.10.1 -->
|
||||
<xs:attribute name="originalWidth" type="xs:int" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="originalHeight" type="xs:int" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="MediaType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="music"/>
|
||||
<xs:enumeration value="podcast"/>
|
||||
<xs:enumeration value="audiobook"/>
|
||||
<xs:enumeration value="video"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="UserRating">
|
||||
<xs:restriction base="xs:int">
|
||||
<xs:minInclusive value="1"/>
|
||||
<xs:maxInclusive value="5"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="AverageRating">
|
||||
<xs:restriction base="xs:double">
|
||||
<xs:minInclusive value="1.0"/>
|
||||
<xs:maxInclusive value="5.0"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="NowPlaying">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:NowPlayingEntry" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="NowPlayingEntry">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:Child">
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="minutesAgo" type="xs:int" use="required"/>
|
||||
<xs:attribute name="playerId" type="xs:int" use="required"/>
|
||||
<xs:attribute name="playerName" type="xs:string" use="optional"/>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<!--Deprecated-->
|
||||
<xs:complexType name="SearchResult">
|
||||
<xs:sequence>
|
||||
<xs:element name="match" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="offset" type="xs:int" use="required"/>
|
||||
<xs:attribute name="totalHits" type="xs:int" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SearchResult2">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SearchResult3">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Playlists">
|
||||
<xs:sequence>
|
||||
<xs:element name="playlist" type="sub:Playlist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Playlist">
|
||||
<xs:sequence>
|
||||
<xs:element name="allowedUser" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> <!--Added in 1.8.0-->
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="comment" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="owner" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="public" type="xs:boolean" use="optional"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="songCount" type="xs:int" use="required"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="duration" type="xs:int" use="required"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="created" type="xs:dateTime" use="required"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="changed" type="xs:dateTime" use="required"/> <!--Added in 1.13.0-->
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/> <!--Added in 1.11.0-->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="PlaylistWithSongs">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:Playlist">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="JukeboxStatus">
|
||||
<xs:attribute name="currentIndex" type="xs:int" use="required"/>
|
||||
<xs:attribute name="playing" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="gain" type="xs:float" use="required"/>
|
||||
<xs:attribute name="position" type="xs:int" use="optional"/> <!--Added in 1.7.0-->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="JukeboxPlaylist">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:JukeboxStatus">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ChatMessages">
|
||||
<xs:sequence>
|
||||
<xs:element name="chatMessage" type="sub:ChatMessage" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ChatMessage">
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="time" type="xs:long" use="required"/>
|
||||
<xs:attribute name="message" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumList">
|
||||
<xs:sequence>
|
||||
<xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumList2">
|
||||
<xs:sequence>
|
||||
<xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Songs">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Lyrics" mixed="true">
|
||||
<xs:attribute name="artist" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="title" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Podcasts">
|
||||
<xs:sequence>
|
||||
<xs:element name="channel" type="sub:PodcastChannel" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="PodcastChannel">
|
||||
<xs:sequence>
|
||||
<xs:element name="episode" type="sub:PodcastEpisode" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="url" type="xs:string" use="required"/>
|
||||
<xs:attribute name="title" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="description" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="originalImageUrl" type="xs:string" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="status" type="sub:PodcastStatus" use="required"/>
|
||||
<xs:attribute name="errorMessage" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="NewestPodcasts">
|
||||
<xs:sequence>
|
||||
<xs:element name="episode" type="sub:PodcastEpisode" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="PodcastEpisode">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:Child">
|
||||
<xs:attribute name="streamId" type="xs:string" use="optional"/> <!-- Use this ID for streaming the podcast. -->
|
||||
<xs:attribute name="channelId" type="xs:string" use="required"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="description" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="status" type="sub:PodcastStatus" use="required"/>
|
||||
<xs:attribute name="publishDate" type="xs:dateTime" use="optional"/>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="PodcastStatus">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="new"/>
|
||||
<xs:enumeration value="downloading"/>
|
||||
<xs:enumeration value="completed"/>
|
||||
<xs:enumeration value="error"/>
|
||||
<xs:enumeration value="deleted"/>
|
||||
<xs:enumeration value="skipped"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="InternetRadioStations">
|
||||
<xs:sequence>
|
||||
<xs:element name="internetRadioStation" type="sub:InternetRadioStation" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="InternetRadioStation">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="streamUrl" type="xs:string" use="required"/>
|
||||
<xs:attribute name="homePageUrl" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Bookmarks">
|
||||
<xs:sequence>
|
||||
<xs:element name="bookmark" type="sub:Bookmark" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Bookmark">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="1" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="position" type="xs:long" use="required"/> <!-- In milliseconds -->
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="comment" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="created" type="xs:dateTime" use="required"/>
|
||||
<xs:attribute name="changed" type="xs:dateTime" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="PlayQueue">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="current" type="xs:int" use="optional"/> <!-- ID of currently playing track -->
|
||||
<xs:attribute name="position" type="xs:long" use="optional"/> <!-- Position in milliseconds of currently playing track -->
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="changed" type="xs:dateTime" use="required"/>
|
||||
<xs:attribute name="changedBy" type="xs:string" use="required"/> <!-- Name of client app -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Shares">
|
||||
<xs:sequence>
|
||||
<xs:element name="share" type="sub:Share" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Share">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="url" type="xs:string" use="required"/>
|
||||
<xs:attribute name="description" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="created" type="xs:dateTime" use="required"/>
|
||||
<xs:attribute name="expires" type="xs:dateTime" use="optional"/>
|
||||
<xs:attribute name="lastVisited" type="xs:dateTime" use="optional"/>
|
||||
<xs:attribute name="visitCount" type="xs:int" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Starred">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumInfo">
|
||||
<xs:sequence>
|
||||
<xs:element name="notes" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="musicBrainzId" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="lastFmUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="smallImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="mediumImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="largeImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistInfoBase">
|
||||
<xs:sequence>
|
||||
<xs:element name="biography" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="musicBrainzId" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="lastFmUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="smallImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="mediumImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="largeImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistInfo">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:ArtistInfoBase">
|
||||
<xs:sequence>
|
||||
<xs:element name="similarArtist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistInfo2">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:ArtistInfoBase">
|
||||
<xs:sequence>
|
||||
<xs:element name="similarArtist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SimilarSongs">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SimilarSongs2">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="TopSongs">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Starred2">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="License">
|
||||
<xs:attribute name="valid" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="email" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="licenseExpires" type="xs:dateTime" use="optional"/>
|
||||
<xs:attribute name="trialExpires" type="xs:dateTime" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ScanStatus">
|
||||
<xs:attribute name="scanning" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="count" type="xs:long" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Users">
|
||||
<xs:sequence>
|
||||
<xs:element name="user" type="sub:User" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="User">
|
||||
<xs:sequence>
|
||||
<xs:element name="folder" type="xs:int" minOccurs="0" maxOccurs="unbounded"/> <!-- Added in 1.12.0 -->
|
||||
</xs:sequence>
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="email" type="xs:string" use="optional"/> <!-- Added in 1.6.0 -->
|
||||
<xs:attribute name="scrobblingEnabled" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
|
||||
<xs:attribute name="maxBitRate" type="xs:int" use="optional"/> <!-- In Kbps, added in 1.13.0 -->
|
||||
<xs:attribute name="adminRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="settingsRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="downloadRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="uploadRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="playlistRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="coverArtRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="commentRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="podcastRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="streamRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="jukeboxRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="shareRole" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
|
||||
<xs:attribute name="videoConversionRole" type="xs:boolean" use="required"/> <!-- Added in 1.14.0 -->
|
||||
<xs:attribute name="avatarLastChanged" type="xs:dateTime" use="optional"/> <!-- Added in 1.14.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Error">
|
||||
<xs:attribute name="code" type="xs:int" use="required"/>
|
||||
<xs:attribute name="message" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
</xs:schema>
|
640
api_object_generator/api_specs/subsonic-rest-api-1.16.1.xsd
Normal file
640
api_object_generator/api_specs/subsonic-rest-api-1.16.1.xsd
Normal file
@@ -0,0 +1,640 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:sub="http://subsonic.org/restapi"
|
||||
targetNamespace="http://subsonic.org/restapi"
|
||||
attributeFormDefault="unqualified"
|
||||
elementFormDefault="qualified"
|
||||
version="1.16.1">
|
||||
|
||||
<xs:element name="subsonic-response" type="sub:Response"/>
|
||||
|
||||
<xs:complexType name="Response">
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="musicFolders" type="sub:MusicFolders" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="indexes" type="sub:Indexes" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="directory" type="sub:Directory" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="genres" type="sub:Genres" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="artists" type="sub:ArtistsID3" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="artist" type="sub:ArtistWithAlbumsID3" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="album" type="sub:AlbumWithSongsID3" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="videos" type="sub:Videos" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="videoInfo" type="sub:VideoInfo" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="nowPlaying" type="sub:NowPlaying" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="searchResult" type="sub:SearchResult" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="searchResult2" type="sub:SearchResult2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="searchResult3" type="sub:SearchResult3" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="playlists" type="sub:Playlists" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="playlist" type="sub:PlaylistWithSongs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="jukeboxStatus" type="sub:JukeboxStatus" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="jukeboxPlaylist" type="sub:JukeboxPlaylist" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="license" type="sub:License" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="users" type="sub:Users" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="user" type="sub:User" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="chatMessages" type="sub:ChatMessages" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="albumList" type="sub:AlbumList" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="albumList2" type="sub:AlbumList2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="randomSongs" type="sub:Songs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="songsByGenre" type="sub:Songs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="lyrics" type="sub:Lyrics" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="podcasts" type="sub:Podcasts" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="newestPodcasts" type="sub:NewestPodcasts" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="internetRadioStations" type="sub:InternetRadioStations" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="bookmarks" type="sub:Bookmarks" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="playQueue" type="sub:PlayQueue" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="shares" type="sub:Shares" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="starred" type="sub:Starred" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="starred2" type="sub:Starred2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="albumInfo" type="sub:AlbumInfo" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="artistInfo" type="sub:ArtistInfo" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="artistInfo2" type="sub:ArtistInfo2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="similarSongs" type="sub:SimilarSongs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="similarSongs2" type="sub:SimilarSongs2" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="topSongs" type="sub:TopSongs" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="scanStatus" type="sub:ScanStatus" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="error" type="sub:Error" minOccurs="1" maxOccurs="1"/>
|
||||
</xs:choice>
|
||||
<xs:attribute name="status" type="sub:ResponseStatus" use="required"/>
|
||||
<xs:attribute name="version" type="sub:Version" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="ResponseStatus">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="ok"/>
|
||||
<xs:enumeration value="failed"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="Version">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="\d+\.\d+\.\d+"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="MusicFolders">
|
||||
<xs:sequence>
|
||||
<xs:element name="musicFolder" type="sub:MusicFolder" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="MusicFolder">
|
||||
<xs:attribute name="id" type="xs:int" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Indexes">
|
||||
<xs:sequence>
|
||||
<xs:element name="shortcut" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="index" type="sub:Index" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="child" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/> <!-- Added in 1.7.0 -->
|
||||
</xs:sequence>
|
||||
<xs:attribute name="lastModified" type="xs:long" use="required"/>
|
||||
<xs:attribute name="ignoredArticles" type="xs:string" use="required"/> <!-- Added in 1.10.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Index">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Artist">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="artistImageUrl" type="xs:string" use="optional"/> <!-- Added in 1.16.1 -->
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.10.1 -->
|
||||
<xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Genres">
|
||||
<xs:sequence>
|
||||
<xs:element name="genre" type="sub:Genre" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Genre" mixed="true">
|
||||
<xs:attribute name="songCount" type="xs:int" use="required"/> <!-- Added in 1.10.2 -->
|
||||
<xs:attribute name="albumCount" type="xs:int" use="required"/> <!-- Added in 1.10.2 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistsID3">
|
||||
<xs:sequence>
|
||||
<xs:element name="index" type="sub:IndexID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="ignoredArticles" type="xs:string" use="required"/> <!-- Added in 1.10.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="IndexID3">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistID3">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="artistImageUrl" type="xs:string" use="optional"/> <!-- Added in 1.16.1 -->
|
||||
<xs:attribute name="albumCount" type="xs:int" use="required"/>
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistWithAlbumsID3">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:ArtistID3">
|
||||
<xs:sequence>
|
||||
<xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumID3">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="artist" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="artistId" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="songCount" type="xs:int" use="required"/>
|
||||
<xs:attribute name="duration" type="xs:int" use="required"/>
|
||||
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
|
||||
<xs:attribute name="created" type="xs:dateTime" use="required"/>
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/>
|
||||
<xs:attribute name="year" type="xs:int" use="optional"/> <!-- Added in 1.10.1 -->
|
||||
<xs:attribute name="genre" type="xs:string" use="optional"/> <!-- Added in 1.10.1 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumWithSongsID3">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:AlbumID3">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Videos">
|
||||
<xs:sequence>
|
||||
<xs:element name="video" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="VideoInfo">
|
||||
<xs:sequence>
|
||||
<xs:element name="captions" type="sub:Captions" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="audioTrack" type="sub:AudioTrack" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="conversion" type="sub:VideoConversion" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Captions">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AudioTrack">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="languageCode" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="VideoConversion">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="bitRate" type="xs:int" use="optional"/> <!-- In Kbps -->
|
||||
<xs:attribute name="audioTrackId" type="xs:int" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Directory">
|
||||
<xs:sequence>
|
||||
<xs:element name="child" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="parent" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.10.1 -->
|
||||
<xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Child">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="parent" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="isDir" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="title" type="xs:string" use="required"/>
|
||||
<xs:attribute name="album" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="artist" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="track" type="xs:int" use="optional"/>
|
||||
<xs:attribute name="year" type="xs:int" use="optional"/>
|
||||
<xs:attribute name="genre" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="size" type="xs:long" use="optional"/>
|
||||
<xs:attribute name="contentType" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="suffix" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="transcodedContentType" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="transcodedSuffix" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="duration" type="xs:int" use="optional"/>
|
||||
<xs:attribute name="bitRate" type="xs:int" use="optional"/>
|
||||
<xs:attribute name="path" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="isVideo" type="xs:boolean" use="optional"/> <!-- Added in 1.4.1 -->
|
||||
<xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.6.0 -->
|
||||
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.6.0 -->
|
||||
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
|
||||
<xs:attribute name="discNumber" type="xs:int" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="created" type="xs:dateTime" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="albumId" type="xs:string" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="artistId" type="xs:string" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="type" type="sub:MediaType" use="optional"/> <!-- Added in 1.8.0 -->
|
||||
<xs:attribute name="bookmarkPosition" type="xs:long" use="optional"/> <!-- In millis. Added in 1.10.1 -->
|
||||
<xs:attribute name="originalWidth" type="xs:int" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="originalHeight" type="xs:int" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="MediaType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="music"/>
|
||||
<xs:enumeration value="podcast"/>
|
||||
<xs:enumeration value="audiobook"/>
|
||||
<xs:enumeration value="video"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="UserRating">
|
||||
<xs:restriction base="xs:int">
|
||||
<xs:minInclusive value="1"/>
|
||||
<xs:maxInclusive value="5"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="AverageRating">
|
||||
<xs:restriction base="xs:double">
|
||||
<xs:minInclusive value="1.0"/>
|
||||
<xs:maxInclusive value="5.0"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="NowPlaying">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:NowPlayingEntry" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="NowPlayingEntry">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:Child">
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="minutesAgo" type="xs:int" use="required"/>
|
||||
<xs:attribute name="playerId" type="xs:int" use="required"/>
|
||||
<xs:attribute name="playerName" type="xs:string" use="optional"/>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<!--Deprecated-->
|
||||
<xs:complexType name="SearchResult">
|
||||
<xs:sequence>
|
||||
<xs:element name="match" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="offset" type="xs:int" use="required"/>
|
||||
<xs:attribute name="totalHits" type="xs:int" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SearchResult2">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SearchResult3">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Playlists">
|
||||
<xs:sequence>
|
||||
<xs:element name="playlist" type="sub:Playlist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Playlist">
|
||||
<xs:sequence>
|
||||
<xs:element name="allowedUser" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> <!--Added in 1.8.0-->
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="comment" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="owner" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="public" type="xs:boolean" use="optional"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="songCount" type="xs:int" use="required"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="duration" type="xs:int" use="required"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="created" type="xs:dateTime" use="required"/> <!--Added in 1.8.0-->
|
||||
<xs:attribute name="changed" type="xs:dateTime" use="required"/> <!--Added in 1.13.0-->
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/> <!--Added in 1.11.0-->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="PlaylistWithSongs">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:Playlist">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="JukeboxStatus">
|
||||
<xs:attribute name="currentIndex" type="xs:int" use="required"/>
|
||||
<xs:attribute name="playing" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="gain" type="xs:float" use="required"/>
|
||||
<xs:attribute name="position" type="xs:int" use="optional"/> <!--Added in 1.7.0-->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="JukeboxPlaylist">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:JukeboxStatus">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ChatMessages">
|
||||
<xs:sequence>
|
||||
<xs:element name="chatMessage" type="sub:ChatMessage" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ChatMessage">
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="time" type="xs:long" use="required"/>
|
||||
<xs:attribute name="message" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumList">
|
||||
<xs:sequence>
|
||||
<xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumList2">
|
||||
<xs:sequence>
|
||||
<xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Songs">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Lyrics" mixed="true">
|
||||
<xs:attribute name="artist" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="title" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Podcasts">
|
||||
<xs:sequence>
|
||||
<xs:element name="channel" type="sub:PodcastChannel" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="PodcastChannel">
|
||||
<xs:sequence>
|
||||
<xs:element name="episode" type="sub:PodcastEpisode" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="url" type="xs:string" use="required"/>
|
||||
<xs:attribute name="title" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="description" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="coverArt" type="xs:string" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="originalImageUrl" type="xs:string" use="optional"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="status" type="sub:PodcastStatus" use="required"/>
|
||||
<xs:attribute name="errorMessage" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="NewestPodcasts">
|
||||
<xs:sequence>
|
||||
<xs:element name="episode" type="sub:PodcastEpisode" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="PodcastEpisode">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:Child">
|
||||
<xs:attribute name="streamId" type="xs:string" use="optional"/> <!-- Use this ID for streaming the podcast. -->
|
||||
<xs:attribute name="channelId" type="xs:string" use="required"/> <!-- Added in 1.13.0 -->
|
||||
<xs:attribute name="description" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="status" type="sub:PodcastStatus" use="required"/>
|
||||
<xs:attribute name="publishDate" type="xs:dateTime" use="optional"/>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="PodcastStatus">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="new"/>
|
||||
<xs:enumeration value="downloading"/>
|
||||
<xs:enumeration value="completed"/>
|
||||
<xs:enumeration value="error"/>
|
||||
<xs:enumeration value="deleted"/>
|
||||
<xs:enumeration value="skipped"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="InternetRadioStations">
|
||||
<xs:sequence>
|
||||
<xs:element name="internetRadioStation" type="sub:InternetRadioStation" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="InternetRadioStation">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="streamUrl" type="xs:string" use="required"/>
|
||||
<xs:attribute name="homePageUrl" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Bookmarks">
|
||||
<xs:sequence>
|
||||
<xs:element name="bookmark" type="sub:Bookmark" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Bookmark">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="1" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="position" type="xs:long" use="required"/> <!-- In milliseconds -->
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="comment" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="created" type="xs:dateTime" use="required"/>
|
||||
<xs:attribute name="changed" type="xs:dateTime" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="PlayQueue">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="current" type="xs:int" use="optional"/> <!-- ID of currently playing track -->
|
||||
<xs:attribute name="position" type="xs:long" use="optional"/> <!-- Position in milliseconds of currently playing track -->
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="changed" type="xs:dateTime" use="required"/>
|
||||
<xs:attribute name="changedBy" type="xs:string" use="required"/> <!-- Name of client app -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Shares">
|
||||
<xs:sequence>
|
||||
<xs:element name="share" type="sub:Share" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Share">
|
||||
<xs:sequence>
|
||||
<xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="url" type="xs:string" use="required"/>
|
||||
<xs:attribute name="description" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="created" type="xs:dateTime" use="required"/>
|
||||
<xs:attribute name="expires" type="xs:dateTime" use="optional"/>
|
||||
<xs:attribute name="lastVisited" type="xs:dateTime" use="optional"/>
|
||||
<xs:attribute name="visitCount" type="xs:int" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Starred">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="AlbumInfo">
|
||||
<xs:sequence>
|
||||
<xs:element name="notes" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="musicBrainzId" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="lastFmUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="smallImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="mediumImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="largeImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistInfoBase">
|
||||
<xs:sequence>
|
||||
<xs:element name="biography" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="musicBrainzId" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="lastFmUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="smallImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="mediumImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="largeImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistInfo">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:ArtistInfoBase">
|
||||
<xs:sequence>
|
||||
<xs:element name="similarArtist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtistInfo2">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="sub:ArtistInfoBase">
|
||||
<xs:sequence>
|
||||
<xs:element name="similarArtist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SimilarSongs">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SimilarSongs2">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="TopSongs">
|
||||
<xs:sequence>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Starred2">
|
||||
<xs:sequence>
|
||||
<xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="License">
|
||||
<xs:attribute name="valid" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="email" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="licenseExpires" type="xs:dateTime" use="optional"/>
|
||||
<xs:attribute name="trialExpires" type="xs:dateTime" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ScanStatus">
|
||||
<xs:attribute name="scanning" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="count" type="xs:long" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Users">
|
||||
<xs:sequence>
|
||||
<xs:element name="user" type="sub:User" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="User">
|
||||
<xs:sequence>
|
||||
<xs:element name="folder" type="xs:int" minOccurs="0" maxOccurs="unbounded"/> <!-- Added in 1.12.0 -->
|
||||
</xs:sequence>
|
||||
<xs:attribute name="username" type="xs:string" use="required"/>
|
||||
<xs:attribute name="email" type="xs:string" use="optional"/> <!-- Added in 1.6.0 -->
|
||||
<xs:attribute name="scrobblingEnabled" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
|
||||
<xs:attribute name="maxBitRate" type="xs:int" use="optional"/> <!-- In Kbps, added in 1.13.0 -->
|
||||
<xs:attribute name="adminRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="settingsRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="downloadRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="uploadRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="playlistRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="coverArtRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="commentRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="podcastRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="streamRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="jukeboxRole" type="xs:boolean" use="required"/>
|
||||
<xs:attribute name="shareRole" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
|
||||
<xs:attribute name="videoConversionRole" type="xs:boolean" use="required"/> <!-- Added in 1.14.0 -->
|
||||
<xs:attribute name="avatarLastChanged" type="xs:dateTime" use="optional"/> <!-- Added in 1.14.0 -->
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="Error">
|
||||
<xs:attribute name="code" type="xs:int" use="required"/>
|
||||
<xs:attribute name="message" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
</xs:schema>
|
@@ -1,27 +1,21 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
import sys
|
||||
import re
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from termcolor import cprint
|
||||
|
||||
print_re = re.compile(r'print\(.*\)')
|
||||
todo_re = re.compile(r'#\s*TODO:?\s*')
|
||||
accounted_for_todo = re.compile(r'#\s*TODO:?\s*\((#\d+)\)')
|
||||
|
||||
|
||||
def check_file(path):
|
||||
print(f'Checking {path.absolute()}...')
|
||||
def check_file(path: Path) -> bool:
|
||||
print(f'Checking {path.absolute()}...') # noqa: T001
|
||||
file = path.open()
|
||||
valid = True
|
||||
|
||||
for i, line in enumerate(file, start=1):
|
||||
if print_re.search(line) and '# allowprint' not in line:
|
||||
cprint(f'{i}: {line}', 'red', end='', attrs=['bold'])
|
||||
valid = False
|
||||
|
||||
if todo_re.search(line) and not accounted_for_todo.search(line):
|
||||
cprint(f'{i}: {line}', 'red', end='', attrs=['bold'])
|
||||
valid = False
|
||||
@@ -33,6 +27,6 @@ def check_file(path):
|
||||
valid = True
|
||||
for path in Path('sublime').glob('**/*.py'):
|
||||
valid &= check_file(path)
|
||||
print()
|
||||
print() # noqa: T001
|
||||
|
||||
sys.exit(0 if valid else 1)
|
||||
|
@@ -1,6 +1,9 @@
|
||||
[flake8]
|
||||
ignore = E402, W503
|
||||
ignore = E402, W503, ANN002, ANN003, ANN101, ANN102, ANN204
|
||||
exclude = .git,__pycache__,build,dist
|
||||
suppress-none-returning = True
|
||||
application-import-names = sublime
|
||||
import-order-style = edited
|
||||
|
||||
[mypy-gi]
|
||||
ignore_missing_imports = True
|
||||
|
@@ -1,14 +1,14 @@
|
||||
#! /usr/bin/env python3
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk # noqa: F401
|
||||
|
||||
import sublime
|
||||
from .app import SublimeMusicApp
|
||||
from sublime.app import SublimeMusicApp
|
||||
|
||||
|
||||
def main():
|
||||
@@ -39,7 +39,7 @@ def main():
|
||||
|
||||
args, unknown_args = parser.parse_known_args()
|
||||
if args.version:
|
||||
print(f'Sublime Music v{sublime.__version__}') # allowprint
|
||||
print(f'Sublime Music v{sublime.__version__}') # noqa: T001
|
||||
return
|
||||
|
||||
min_log_level = getattr(logging, args.loglevel.upper(), None)
|
||||
|
@@ -1,26 +1,25 @@
|
||||
import os
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Notify', '0.7')
|
||||
from gi.repository import Gdk, Gio, GLib, Gtk, Notify, GdkPixbuf
|
||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, Notify
|
||||
|
||||
from .ui.main import MainWindow
|
||||
from .ui.configure_servers import ConfigureServersDialog
|
||||
from .ui.settings import SettingsDialog
|
||||
|
||||
from .dbus_manager import DBusManager, dbus_propagate
|
||||
from .state_manager import ApplicationState, RepeatType
|
||||
from .cache_manager import CacheManager
|
||||
from .dbus_manager import dbus_propagate, DBusManager
|
||||
from .players import ChromecastPlayer, MPVPlayer, PlayerEvent
|
||||
from .server.api_objects import Child, Directory
|
||||
from .players import PlayerEvent, MPVPlayer, ChromecastPlayer
|
||||
from .state_manager import ApplicationState, RepeatType
|
||||
from .ui.configure_servers import ConfigureServersDialog
|
||||
from .ui.main import MainWindow
|
||||
from .ui.settings import SettingsDialog
|
||||
|
||||
|
||||
class SublimeMusicApp(Gtk.Application):
|
||||
def __init__(self, config_file):
|
||||
def __init__(self, config_file: str):
|
||||
super().__init__(application_id="com.sumnerevans.sublimemusic")
|
||||
Notify.init('Sublime Music')
|
||||
|
||||
@@ -912,11 +911,12 @@ class SublimeMusicApp(Gtk.Application):
|
||||
song_idx = self.state.play_queue.index(song.id)
|
||||
prefetch_idxs = []
|
||||
for i in range(self.state.config.prefetch_amount):
|
||||
prefetch_idx = song_idx + 1 + i
|
||||
play_queue_len = len(self.state.play_queue)
|
||||
prefetch_idx: int = song_idx + 1 + i
|
||||
play_queue_len: int = len(self.state.play_queue)
|
||||
if (self.state.repeat_type == RepeatType.REPEAT_QUEUE
|
||||
or prefetch_idx < play_queue_len):
|
||||
prefetch_idxs.append(prefetch_idx % play_queue_len)
|
||||
prefetch_idxs.append(
|
||||
prefetch_idx % play_queue_len) # noqa: S001
|
||||
CacheManager.batch_download_songs(
|
||||
[self.state.play_queue[i] for i in prefetch_idxs],
|
||||
before_download=lambda: GLib.idle_add(self.update_window),
|
||||
|
@@ -1,57 +1,50 @@
|
||||
import os
|
||||
import logging
|
||||
import glob
|
||||
import itertools
|
||||
import threading
|
||||
import shutil
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
from functools import lru_cache
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from time import sleep
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, Future
|
||||
from enum import EnumMeta, Enum
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from enum import Enum, EnumMeta
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
DefaultDict,
|
||||
Generic,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Union,
|
||||
Callable,
|
||||
Set,
|
||||
DefaultDict,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import requests
|
||||
|
||||
from fuzzywuzzy import fuzz
|
||||
|
||||
from .config import AppConfiguration, ServerConfiguration
|
||||
from .server import Server
|
||||
from .server.api_object import APIObject
|
||||
from .server.api_objects import (
|
||||
Playlist,
|
||||
PlaylistWithSongs,
|
||||
Child,
|
||||
Genre,
|
||||
|
||||
# Non-ID3 versions
|
||||
AlbumID3,
|
||||
AlbumWithSongsID3,
|
||||
Artist,
|
||||
Directory,
|
||||
|
||||
# ID3 versions
|
||||
ArtistID3,
|
||||
ArtistInfo2,
|
||||
ArtistWithAlbumsID3,
|
||||
AlbumID3,
|
||||
AlbumWithSongsID3,
|
||||
Child,
|
||||
Directory,
|
||||
Genre,
|
||||
Playlist,
|
||||
PlaylistWithSongs,
|
||||
)
|
||||
|
||||
|
||||
@@ -60,7 +53,7 @@ class Singleton(type):
|
||||
Metaclass for :class:`CacheManager` so that it can be used like a
|
||||
singleton.
|
||||
"""
|
||||
def __getattr__(cls, name):
|
||||
def __getattr__(cls, name: str) -> Any:
|
||||
if not CacheManager._instance:
|
||||
return None
|
||||
# If the cache has a function to do the thing we want, use it. If
|
||||
@@ -82,7 +75,7 @@ class SongCacheStatus(Enum):
|
||||
|
||||
|
||||
@lru_cache(maxsize=8192)
|
||||
def similarity_ratio(query: str, string: str):
|
||||
def similarity_ratio(query: str, string: str) -> int:
|
||||
"""
|
||||
Return the :class:`fuzzywuzzy.fuzz.partial_ratio` between the ``query`` and
|
||||
the given ``string``.
|
||||
@@ -96,17 +89,20 @@ def similarity_ratio(query: str, string: str):
|
||||
return fuzz.partial_ratio(query.lower(), string.lower())
|
||||
|
||||
|
||||
S = TypeVar('S')
|
||||
|
||||
|
||||
class SearchResult:
|
||||
"""
|
||||
An object representing the aggregate results of a search which can include
|
||||
both server and local results.
|
||||
"""
|
||||
_artist: Set[Union[Artist, ArtistID3]] = set()
|
||||
_album: Set[Union[Child, AlbumID3]] = set()
|
||||
_artist: Set[ArtistID3] = set()
|
||||
_album: Set[AlbumID3] = set()
|
||||
_song: Set[Child] = set()
|
||||
_playlist: Set[Playlist] = set()
|
||||
|
||||
def __init__(self, query):
|
||||
def __init__(self, query: str):
|
||||
self.query = query
|
||||
|
||||
def add_results(self, result_type: str, results: Iterable):
|
||||
@@ -124,13 +120,17 @@ class SearchResult:
|
||||
getattr(getattr(self, member, set()), 'union')(set(results)),
|
||||
)
|
||||
|
||||
def _to_result(self, it, transform):
|
||||
def _to_result(
|
||||
self,
|
||||
it: Iterable[S],
|
||||
transform: Callable[[S], str],
|
||||
) -> List[S]:
|
||||
all_results = sorted(
|
||||
((similarity_ratio(self.query, transform(x)), x) for x in it),
|
||||
key=lambda rx: rx[0],
|
||||
reverse=True,
|
||||
)
|
||||
result = []
|
||||
result: List[S] = []
|
||||
for ratio, x in all_results:
|
||||
if ratio > 60 and len(result) < 20:
|
||||
result.append(x)
|
||||
@@ -181,9 +181,9 @@ class CacheManager(metaclass=Singleton):
|
||||
around a Future, but it can also resolve immediately if the data
|
||||
already exists.
|
||||
"""
|
||||
data = None
|
||||
future = None
|
||||
on_cancel = None
|
||||
data: Optional[T] = None
|
||||
future: Optional[Future] = None
|
||||
on_cancel: Optional[Callable[[], None]] = None
|
||||
|
||||
@staticmethod
|
||||
def from_data(data: T) -> 'CacheManager.Result[T]':
|
||||
@@ -193,10 +193,10 @@ class CacheManager(metaclass=Singleton):
|
||||
|
||||
@staticmethod
|
||||
def from_server(
|
||||
download_fn,
|
||||
before_download=None,
|
||||
after_download=None,
|
||||
on_cancel=None,
|
||||
download_fn: Callable[[], T],
|
||||
before_download: Callable[[], Any] = None,
|
||||
after_download: Callable[[T], Any] = None,
|
||||
on_cancel: Callable[[], Any] = None,
|
||||
) -> 'CacheManager.Result[T]':
|
||||
result: 'CacheManager.Result[T]' = CacheManager.Result()
|
||||
|
||||
@@ -208,9 +208,9 @@ class CacheManager(metaclass=Singleton):
|
||||
result.future = CacheManager.executor.submit(future_fn)
|
||||
result.on_cancel = on_cancel
|
||||
|
||||
if after_download:
|
||||
if after_download is not None:
|
||||
result.future.add_done_callback(
|
||||
lambda f: after_download(f.result()))
|
||||
lambda f: after_download and after_download(f.result()))
|
||||
|
||||
return result
|
||||
|
||||
@@ -224,8 +224,8 @@ class CacheManager(metaclass=Singleton):
|
||||
'CacheManager.Result did not have either a data or future '
|
||||
'member.')
|
||||
|
||||
def add_done_callback(self, fn, *args):
|
||||
if self.is_future:
|
||||
def add_done_callback(self, fn: Callable, *args):
|
||||
if self.future is not None:
|
||||
self.future.add_done_callback(fn, *args)
|
||||
else:
|
||||
# Run the function immediately if it's not a future.
|
||||
@@ -668,7 +668,7 @@ class CacheManager(metaclass=Singleton):
|
||||
return CacheManager.Result.from_data(
|
||||
self.cache[cache_name][artist_id])
|
||||
|
||||
def after_download(artist_info):
|
||||
def after_download(artist_info: Optional[ArtistInfo2]):
|
||||
if not artist_info:
|
||||
return
|
||||
|
||||
@@ -677,7 +677,8 @@ class CacheManager(metaclass=Singleton):
|
||||
self.save_cache_info()
|
||||
|
||||
return CacheManager.Result.from_server(
|
||||
lambda: self.server.get_artist_info2(id=artist_id),
|
||||
lambda:
|
||||
(self.server.get_artist_info2(id=artist_id) or ArtistInfo2()),
|
||||
before_download=before_download,
|
||||
after_download=after_download,
|
||||
)
|
||||
@@ -881,9 +882,8 @@ class CacheManager(metaclass=Singleton):
|
||||
) -> 'CacheManager.Result[Optional[str]]':
|
||||
if id is None:
|
||||
art_path = 'ui/images/default-album-art.png'
|
||||
return CacheManager.Result.from_data(str(
|
||||
Path(__file__).parent.joinpath(art_path)
|
||||
))
|
||||
return CacheManager.Result.from_data(
|
||||
str(Path(__file__).parent.joinpath(art_path)))
|
||||
return self.return_cached_or_download(
|
||||
f'cover_art/{id}_{size}',
|
||||
lambda: self.server.get_cover_art(id, str(size)),
|
||||
@@ -971,10 +971,10 @@ class CacheManager(metaclass=Singleton):
|
||||
query,
|
||||
search_callback: Callable[[SearchResult, bool], None],
|
||||
before_download: Callable[[], None] = lambda: None,
|
||||
):
|
||||
) -> 'CacheManager.Result':
|
||||
if query == '':
|
||||
search_callback(SearchResult(''), True)
|
||||
return
|
||||
return CacheManager.from_data(None)
|
||||
|
||||
before_download()
|
||||
|
||||
@@ -1033,9 +1033,7 @@ class CacheManager(metaclass=Singleton):
|
||||
cancelled = True
|
||||
|
||||
return CacheManager.Result.from_server(
|
||||
do_search,
|
||||
on_cancel=on_cancel,
|
||||
)
|
||||
do_search, on_cancel=on_cancel)
|
||||
|
||||
def get_cached_status(self, song: Child) -> SongCacheStatus:
|
||||
cache_path = self.calculate_abs_path(song.path)
|
||||
@@ -1055,7 +1053,11 @@ class CacheManager(metaclass=Singleton):
|
||||
raise Exception('Do not instantiate the CacheManager.')
|
||||
|
||||
@staticmethod
|
||||
def reset(app_config, server_config, current_ssids: Set[str]):
|
||||
def reset(
|
||||
app_config: AppConfiguration,
|
||||
server_config: ServerConfiguration,
|
||||
current_ssids: Set[str],
|
||||
):
|
||||
CacheManager._instance = CacheManager.__CacheManagerInternal(
|
||||
app_config,
|
||||
server_config,
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import os
|
||||
import logging
|
||||
import keyring
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from typing import List, Optional
|
||||
import keyring
|
||||
|
||||
|
||||
class ServerConfiguration:
|
||||
@@ -17,14 +17,14 @@ class ServerConfiguration:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name='Default',
|
||||
server_address='http://yourhost',
|
||||
local_network_address='',
|
||||
local_network_ssid='',
|
||||
username='',
|
||||
password='',
|
||||
sync_enabled=True,
|
||||
disable_cert_verify=False,
|
||||
name: str = 'Default',
|
||||
server_address: str = 'http://yourhost',
|
||||
local_network_address: str = '',
|
||||
local_network_ssid: str = '',
|
||||
username: str = '',
|
||||
password: str = '',
|
||||
sync_enabled: bool = True,
|
||||
disable_cert_verify: bool = False,
|
||||
):
|
||||
self.name = name
|
||||
self.server_address = server_address
|
||||
@@ -43,7 +43,7 @@ class ServerConfiguration:
|
||||
pass
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
def password(self) -> str:
|
||||
return keyring.get_password(
|
||||
'com.sumnerevans.SublimeMusic',
|
||||
f'{self.username}@{self.server_address}',
|
||||
@@ -64,7 +64,7 @@ class AppConfiguration:
|
||||
version: int = 2
|
||||
serve_over_lan: bool = True
|
||||
|
||||
def to_json(self):
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
exclude = ('servers')
|
||||
json_object = {
|
||||
k: getattr(self, k)
|
||||
@@ -88,7 +88,7 @@ class AppConfiguration:
|
||||
self.version = 2
|
||||
|
||||
@property
|
||||
def cache_location(self):
|
||||
def cache_location(self) -> str:
|
||||
if (hasattr(self, '_cache_location')
|
||||
and self._cache_location is not None
|
||||
and self._cache_location != ''):
|
||||
|
@@ -8,8 +8,8 @@ from typing import Dict
|
||||
from deepdiff import DeepDiff
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from .state_manager import RepeatType
|
||||
from .cache_manager import CacheManager
|
||||
from .state_manager import RepeatType
|
||||
|
||||
|
||||
def dbus_propagate(param_self=None):
|
||||
|
@@ -1,18 +1,19 @@
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from enum import EnumMeta
|
||||
import typing
|
||||
from typing import Dict, List, Type
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
from dateutil import parser
|
||||
|
||||
|
||||
def from_json(cls, data):
|
||||
def from_json(template_type: Any, data: Any) -> Any:
|
||||
"""
|
||||
Converts data from a JSON parse into Python data structures.
|
||||
Converts data from a JSON parse into an instantiation of the Python object
|
||||
specified by template_type.
|
||||
|
||||
Arguments:
|
||||
|
||||
cls: the template class to deserialize into
|
||||
template_type: the template type to deserialize into
|
||||
data: the data to deserialize to the class
|
||||
"""
|
||||
# Approach for deserialization here:
|
||||
@@ -20,50 +21,49 @@ def from_json(cls, data):
|
||||
|
||||
# If it's a forward reference, evaluate it to figure out the actual
|
||||
# type. This allows for types that have to be put into a string.
|
||||
if isinstance(cls, typing.ForwardRef):
|
||||
cls = cls._evaluate(globals(), locals())
|
||||
if isinstance(template_type, typing.ForwardRef): # type: ignore
|
||||
template_type = template_type._evaluate(globals(), locals())
|
||||
|
||||
annotations: Dict[str, Type] = getattr(cls, '__annotations__', {})
|
||||
annotations: Dict[str,
|
||||
Type] = getattr(template_type, '__annotations__', {})
|
||||
|
||||
# Handle primitive of objects
|
||||
instance: Any = None
|
||||
if data is None:
|
||||
instance = None
|
||||
# Handle generics. List[*], Dict[*, *] in particular.
|
||||
elif type(cls) == typing._GenericAlias:
|
||||
elif type(template_type) == typing._GenericAlias: # type: ignore
|
||||
# Having to use this because things changed in Python 3.7.
|
||||
class_name = cls._name
|
||||
class_name = template_type._name
|
||||
|
||||
# This is not very elegant since it doesn't allow things which sublass
|
||||
# from List or Dict. For my purposes, this doesn't matter.
|
||||
if class_name == 'List':
|
||||
list_type = cls.__args__[0]
|
||||
instance: List[list_type] = list()
|
||||
for value in data:
|
||||
instance.append(from_json(list_type, value))
|
||||
inner_type = template_type.__args__[0]
|
||||
instance = [from_json(inner_type, value) for value in data]
|
||||
|
||||
elif class_name == 'Dict':
|
||||
key_type, val_type = cls.__args__
|
||||
instance: Dict[key_type, val_type] = dict()
|
||||
for key, value in data.items():
|
||||
key = from_json(key_type, key)
|
||||
value = from_json(val_type, value)
|
||||
instance[key] = value
|
||||
key_type, val_type = template_type.__args__
|
||||
instance = {
|
||||
from_json(key_type, key): from_json(val_type, value)
|
||||
for key, value in data.items()
|
||||
}
|
||||
else:
|
||||
raise Exception(
|
||||
f'Trying to deserialize an unsupported type: {cls._name}')
|
||||
|
||||
elif cls == str or issubclass(cls, str):
|
||||
'Trying to deserialize an unsupported type: {}'.format(
|
||||
template_type._name))
|
||||
elif template_type == str or issubclass(template_type, str):
|
||||
instance = data
|
||||
elif cls == int or issubclass(cls, int):
|
||||
elif template_type == int or issubclass(template_type, int):
|
||||
instance = int(data)
|
||||
elif cls == bool or issubclass(cls, bool):
|
||||
elif template_type == bool or issubclass(template_type, bool):
|
||||
instance = bool(data)
|
||||
elif type(cls) == EnumMeta:
|
||||
elif type(template_type) == EnumMeta:
|
||||
if type(data) == dict:
|
||||
instance = cls(data.get('_value_'))
|
||||
instance = template_type(data.get('_value_'))
|
||||
else:
|
||||
instance = cls(data)
|
||||
elif cls == datetime:
|
||||
instance = template_type(data)
|
||||
elif template_type == datetime:
|
||||
if type(data) == int:
|
||||
instance = datetime.fromtimestamp(data / 1000)
|
||||
else:
|
||||
@@ -72,7 +72,7 @@ def from_json(cls, data):
|
||||
# Handle everything else by first instantiating the class, then adding
|
||||
# all of the sub-elements, recursively calling from_json on them.
|
||||
else:
|
||||
instance: cls = cls()
|
||||
instance = template_type()
|
||||
for field, field_type in annotations.items():
|
||||
value = data.get(field)
|
||||
setattr(instance, field, from_json(field_type, value))
|
||||
|
@@ -5,10 +5,9 @@ import mimetypes
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, Future
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
from time import sleep
|
||||
from typing import Callable, List, Any, Optional
|
||||
from typing import Any, Callable, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
from uuid import UUID
|
||||
|
||||
@@ -16,8 +15,8 @@ import bottle
|
||||
import mpv
|
||||
import pychromecast
|
||||
|
||||
from sublime.config import AppConfiguration
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.config import AppConfiguration
|
||||
from sublime.server.api_objects import Child
|
||||
|
||||
|
||||
@@ -31,9 +30,11 @@ class PlayerEvent:
|
||||
|
||||
|
||||
class Player:
|
||||
_can_hotswap_source: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_timepos_change: Callable[[float], None],
|
||||
on_timepos_change: Callable[[Optional[float]], None],
|
||||
on_track_end: Callable[[], None],
|
||||
on_player_event: Callable[[PlayerEvent], None],
|
||||
config: AppConfiguration,
|
||||
@@ -45,38 +46,38 @@ class Player:
|
||||
self._song_loaded = False
|
||||
|
||||
@property
|
||||
def playing(self):
|
||||
def playing(self) -> bool:
|
||||
return self._is_playing()
|
||||
|
||||
@property
|
||||
def song_loaded(self):
|
||||
def song_loaded(self) -> bool:
|
||||
return self._song_loaded
|
||||
|
||||
@property
|
||||
def can_hotswap_source(self):
|
||||
def can_hotswap_source(self) -> bool:
|
||||
return self._can_hotswap_source
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
def volume(self) -> float:
|
||||
return self._get_volume()
|
||||
|
||||
@volume.setter
|
||||
def volume(self, value):
|
||||
return self._set_volume(value)
|
||||
def volume(self, value: float):
|
||||
self._set_volume(value)
|
||||
|
||||
@property
|
||||
def is_muted(self):
|
||||
def is_muted(self) -> bool:
|
||||
return self._get_is_muted()
|
||||
|
||||
@is_muted.setter
|
||||
def is_muted(self, value):
|
||||
return self._set_is_muted(value)
|
||||
def is_muted(self, value: bool):
|
||||
self._set_is_muted(value)
|
||||
|
||||
def reset(self):
|
||||
raise NotImplementedError(
|
||||
'reset must be implemented by implementor of Player')
|
||||
|
||||
def play_media(self, file_or_url, progress, song):
|
||||
def play_media(self, file_or_url: str, progress: float, song: Child):
|
||||
raise NotImplementedError(
|
||||
'play_media must be implemented by implementor of Player')
|
||||
|
||||
@@ -92,7 +93,7 @@ class Player:
|
||||
raise NotImplementedError(
|
||||
'toggle_play must be implemented by implementor of Player')
|
||||
|
||||
def seek(self, value):
|
||||
def seek(self, value: float):
|
||||
raise NotImplementedError(
|
||||
'seek must be implemented by implementor of Player')
|
||||
|
||||
@@ -104,7 +105,7 @@ class Player:
|
||||
raise NotImplementedError(
|
||||
'_get_volume must be implemented by implementor of Player')
|
||||
|
||||
def _set_volume(self, value):
|
||||
def _set_volume(self, value: float):
|
||||
raise NotImplementedError(
|
||||
'_set_volume must be implemented by implementor of Player')
|
||||
|
||||
@@ -112,7 +113,7 @@ class Player:
|
||||
raise NotImplementedError(
|
||||
'_get_is_muted must be implemented by implementor of Player')
|
||||
|
||||
def _set_is_muted(self, value):
|
||||
def _set_is_muted(self, value: bool):
|
||||
raise NotImplementedError(
|
||||
'_set_is_muted must be implemented by implementor of Player')
|
||||
|
||||
@@ -134,7 +135,7 @@ class MPVPlayer(Player):
|
||||
self._can_hotswap_source = True
|
||||
|
||||
@self.mpv.property_observer('time-pos')
|
||||
def time_observer(_name, value):
|
||||
def time_observer(_: Any, value: Optional[float]):
|
||||
self.on_timepos_change(value)
|
||||
if value is None and self.progress_value_count > 1:
|
||||
self.on_track_end()
|
||||
@@ -145,7 +146,7 @@ class MPVPlayer(Player):
|
||||
with self.progress_value_lock:
|
||||
self.progress_value_count += 1
|
||||
|
||||
def _is_playing(self):
|
||||
def _is_playing(self) -> bool:
|
||||
return not self.mpv.pause
|
||||
|
||||
def reset(self):
|
||||
@@ -153,7 +154,7 @@ class MPVPlayer(Player):
|
||||
with self.progress_value_lock:
|
||||
self.progress_value_count = 0
|
||||
|
||||
def play_media(self, file_or_url, progress, song):
|
||||
def play_media(self, file_or_url: str, progress: float, song: Child):
|
||||
self.had_progress_value = False
|
||||
with self.progress_value_lock:
|
||||
self.progress_value_count = 0
|
||||
@@ -173,20 +174,20 @@ class MPVPlayer(Player):
|
||||
def toggle_play(self):
|
||||
self.mpv.cycle('pause')
|
||||
|
||||
def seek(self, value):
|
||||
def seek(self, value: float):
|
||||
self.mpv.seek(str(value), 'absolute')
|
||||
|
||||
def _set_volume(self, value):
|
||||
def _get_volume(self) -> float:
|
||||
return self._volume
|
||||
|
||||
def _set_volume(self, value: float):
|
||||
self._volume = value
|
||||
self.mpv.volume = self._volume
|
||||
|
||||
def _get_volume(self):
|
||||
return self._volume
|
||||
|
||||
def _get_is_muted(self):
|
||||
def _get_is_muted(self) -> bool:
|
||||
return self._muted
|
||||
|
||||
def _set_is_muted(self, value):
|
||||
def _set_is_muted(self, value: bool):
|
||||
self._muted = value
|
||||
self.mpv.volume = 0 if value else self._volume
|
||||
|
||||
@@ -202,14 +203,14 @@ class ChromecastPlayer(Player):
|
||||
class CastStatusListener:
|
||||
on_new_cast_status: Optional[Callable] = None
|
||||
|
||||
def new_cast_status(self, status):
|
||||
def new_cast_status(self, status: Any):
|
||||
if self.on_new_cast_status:
|
||||
self.on_new_cast_status(status)
|
||||
|
||||
class MediaStatusListener:
|
||||
on_new_media_status: Optional[Callable] = None
|
||||
|
||||
def new_media_status(self, status):
|
||||
def new_media_status(self, status: Any):
|
||||
if self.on_new_media_status:
|
||||
self.on_new_media_status(status)
|
||||
|
||||
@@ -217,18 +218,18 @@ class ChromecastPlayer(Player):
|
||||
media_status_listener = MediaStatusListener()
|
||||
|
||||
class ServerThread(threading.Thread):
|
||||
def __init__(self, host, port):
|
||||
def __init__(self, host: str, port: int):
|
||||
super().__init__()
|
||||
self.daemon = True
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.token = None
|
||||
self.song_id = None
|
||||
self.token: Optional[str] = None
|
||||
self.song_id: Optional[str] = None
|
||||
|
||||
self.app = bottle.Bottle()
|
||||
|
||||
@self.app.route('/')
|
||||
def index():
|
||||
def index() -> str:
|
||||
return '''
|
||||
<h1>Sublime Music Local Music Server</h1>
|
||||
<p>
|
||||
@@ -238,7 +239,7 @@ class ChromecastPlayer(Player):
|
||||
'''
|
||||
|
||||
@self.app.route('/s/<token>')
|
||||
def stream_song(token):
|
||||
def stream_song(token: str) -> bytes:
|
||||
if token != self.token:
|
||||
raise bottle.HTTPError(status=401, body='Invalid token.')
|
||||
|
||||
@@ -254,7 +255,7 @@ class ChromecastPlayer(Player):
|
||||
bottle.response.set_header('Accept-Ranges', 'bytes')
|
||||
return song_buffer.read()
|
||||
|
||||
def set_song_and_token(self, song_id, token):
|
||||
def set_song_and_token(self, song_id: str, token: str):
|
||||
self.song_id = song_id
|
||||
self.token = token
|
||||
|
||||
@@ -265,7 +266,7 @@ class ChromecastPlayer(Player):
|
||||
|
||||
@classmethod
|
||||
def get_chromecasts(cls) -> Future:
|
||||
def do_get_chromecasts():
|
||||
def do_get_chromecasts() -> List[pychromecast.Chromecast]:
|
||||
if not ChromecastPlayer.getting_chromecasts:
|
||||
logging.info('Getting Chromecasts')
|
||||
ChromecastPlayer.getting_chromecasts = True
|
||||
@@ -280,7 +281,7 @@ class ChromecastPlayer(Player):
|
||||
|
||||
return ChromecastPlayer.executor.submit(do_get_chromecasts)
|
||||
|
||||
def set_playing_chromecast(self, uuid):
|
||||
def set_playing_chromecast(self, uuid: str):
|
||||
self.chromecast = next(
|
||||
cc for cc in ChromecastPlayer.chromecasts
|
||||
if cc.device.uuid == UUID(uuid))
|
||||
@@ -294,7 +295,7 @@ class ChromecastPlayer(Player):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_timepos_change: Callable[[float], None],
|
||||
on_timepos_change: Callable[[Optional[float]], None],
|
||||
on_track_end: Callable[[], None],
|
||||
on_player_event: Callable[[PlayerEvent], None],
|
||||
config: AppConfiguration,
|
||||
@@ -335,7 +336,10 @@ class ChromecastPlayer(Player):
|
||||
'0.0.0.0', self.port)
|
||||
self.server_thread.start()
|
||||
|
||||
def on_new_cast_status(self, status):
|
||||
def on_new_cast_status(
|
||||
self,
|
||||
status: pychromecast.socket_client.CastStatus,
|
||||
):
|
||||
self.on_player_event(
|
||||
PlayerEvent(
|
||||
'volume_change',
|
||||
@@ -348,7 +352,10 @@ class ChromecastPlayer(Player):
|
||||
self.on_player_event(PlayerEvent('play_state_change', False))
|
||||
self._song_loaded = False
|
||||
|
||||
def on_new_media_status(self, status):
|
||||
def on_new_media_status(
|
||||
self,
|
||||
status: pychromecast.controllers.media.MediaStatus,
|
||||
):
|
||||
# Detect the end of a track and go to the next one.
|
||||
if (status.idle_reason == 'FINISHED' and status.player_state == 'IDLE'
|
||||
and self._timepos > 0):
|
||||
@@ -382,7 +389,7 @@ class ChromecastPlayer(Player):
|
||||
def start_time_incrementor(self):
|
||||
ChromecastPlayer.executor.submit(self.time_incrementor)
|
||||
|
||||
def wait_for_playing(self, callback, url=None):
|
||||
def wait_for_playing(self, callback: Callable, url: str = None):
|
||||
def do_wait_for_playing():
|
||||
while True:
|
||||
sleep(0.1)
|
||||
@@ -397,7 +404,7 @@ class ChromecastPlayer(Player):
|
||||
|
||||
ChromecastPlayer.executor.submit(do_wait_for_playing)
|
||||
|
||||
def _is_playing(self):
|
||||
def _is_playing(self) -> bool:
|
||||
if not self.chromecast or not self.chromecast.media_controller:
|
||||
return False
|
||||
return self.chromecast.media_controller.status.player_is_playing
|
||||
@@ -455,27 +462,27 @@ class ChromecastPlayer(Player):
|
||||
self.chromecast.media_controller.play()
|
||||
self.wait_for_playing(self.start_time_incrementor)
|
||||
|
||||
def seek(self, value):
|
||||
def seek(self, value: float):
|
||||
do_pause = not self.playing
|
||||
self.chromecast.media_controller.seek(value)
|
||||
if do_pause:
|
||||
self.pause()
|
||||
|
||||
def _set_volume(self, value):
|
||||
# Chromecast volume is in the range [0, 1], not [0, 100].
|
||||
if self.chromecast:
|
||||
self.chromecast.set_volume(value / 100)
|
||||
|
||||
def _get_volume(self, value):
|
||||
def _get_volume(self) -> float:
|
||||
if self.chromecast:
|
||||
return self.chromecast.status.volume_level * 100
|
||||
else:
|
||||
return 100
|
||||
|
||||
def _get_is_muted(self):
|
||||
def _set_volume(self, value: float):
|
||||
# Chromecast volume is in the range [0, 1], not [0, 100].
|
||||
if self.chromecast:
|
||||
self.chromecast.set_volume(value / 100)
|
||||
|
||||
def _get_is_muted(self) -> bool:
|
||||
return self.chromecast.volume_muted
|
||||
|
||||
def _set_is_muted(self, value):
|
||||
def _set_is_muted(self, value: bool):
|
||||
self.chromecast.set_volume_muted(value)
|
||||
|
||||
def shutdown(self):
|
||||
|
@@ -10,7 +10,7 @@ class APIObject:
|
||||
this only supports JSON.
|
||||
"""
|
||||
@classmethod
|
||||
def from_json(cls, data: Dict):
|
||||
def from_json(cls, data: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Creates an :class:`APIObject` by deserializing JSON data into a Python
|
||||
object. This calls the :class:`sublime.from_json.from_json` function
|
||||
@@ -21,7 +21,7 @@ class APIObject:
|
||||
"""
|
||||
return _from_json(cls, data)
|
||||
|
||||
def get(self, field: str, default=None):
|
||||
def get(self, field: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Get the value of ``field`` or ``default``.
|
||||
|
||||
@@ -30,7 +30,7 @@ class APIObject:
|
||||
"""
|
||||
return getattr(self, field, default)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
if isinstance(self, Enum):
|
||||
return super().__repr__()
|
||||
if isinstance(self, str):
|
||||
|
@@ -6,8 +6,9 @@ script or run it on a new API version.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from enum import Enum
|
||||
from typing import Any, List
|
||||
|
||||
from sublime.server.api_object import APIObject
|
||||
|
||||
|
||||
@@ -70,10 +71,10 @@ class Child(APIObject):
|
||||
originalWidth: int
|
||||
originalHeight: int
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Child.{self.id}')
|
||||
|
||||
|
||||
@@ -97,10 +98,10 @@ class AlbumID3(APIObject):
|
||||
year: int
|
||||
genre: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'AlbumID3.{self.id}')
|
||||
|
||||
|
||||
@@ -125,10 +126,10 @@ class AlbumWithSongsID3(APIObject):
|
||||
year: int
|
||||
genre: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'AlbumID3.{self.id}')
|
||||
|
||||
|
||||
@@ -141,10 +142,10 @@ class Artist(APIObject):
|
||||
userRating: UserRating
|
||||
averageRating: AverageRating
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Artist.{self.id}')
|
||||
|
||||
|
||||
@@ -178,10 +179,10 @@ class ArtistID3(APIObject):
|
||||
albumCount: int
|
||||
starred: datetime
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'ArtistID3.{self.id}')
|
||||
|
||||
|
||||
@@ -206,10 +207,10 @@ class ArtistWithAlbumsID3(APIObject):
|
||||
albumCount: int
|
||||
starred: datetime
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'ArtistID3.{self.id}')
|
||||
|
||||
|
||||
@@ -263,10 +264,10 @@ class Directory(APIObject):
|
||||
averageRating: AverageRating
|
||||
playCount: int
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Directory.{self.id}')
|
||||
|
||||
|
||||
@@ -309,10 +310,10 @@ class InternetRadioStation(APIObject):
|
||||
streamUrl: str
|
||||
homePageUrl: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'InternetRadioStation.{self.id}')
|
||||
|
||||
|
||||
@@ -357,10 +358,10 @@ class MusicFolder(APIObject):
|
||||
value: str
|
||||
name: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'MusicFolder.{self.id}')
|
||||
|
||||
|
||||
@@ -417,10 +418,10 @@ class PodcastEpisode(APIObject):
|
||||
originalWidth: int
|
||||
originalHeight: int
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Child.{self.id}')
|
||||
|
||||
|
||||
@@ -467,10 +468,10 @@ class NowPlayingEntry(APIObject):
|
||||
originalWidth: int
|
||||
originalHeight: int
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Child.{self.id}')
|
||||
|
||||
|
||||
@@ -503,10 +504,10 @@ class Playlist(APIObject):
|
||||
changed: datetime
|
||||
coverArt: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Playlist.{self.id}')
|
||||
|
||||
|
||||
@@ -525,10 +526,10 @@ class PlaylistWithSongs(APIObject):
|
||||
changed: datetime
|
||||
coverArt: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Playlist.{self.id}')
|
||||
|
||||
|
||||
@@ -549,10 +550,10 @@ class PodcastChannel(APIObject):
|
||||
status: PodcastStatus
|
||||
errorMessage: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'PodcastChannel.{self.id}')
|
||||
|
||||
|
||||
@@ -605,10 +606,10 @@ class Share(APIObject):
|
||||
lastVisited: datetime
|
||||
visitCount: int
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Share.{self.id}')
|
||||
|
||||
|
||||
@@ -688,10 +689,10 @@ class AudioTrack(APIObject):
|
||||
name: str
|
||||
languageCode: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'AudioTrack.{self.id}')
|
||||
|
||||
|
||||
@@ -700,10 +701,10 @@ class Captions(APIObject):
|
||||
value: str
|
||||
name: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'Captions.{self.id}')
|
||||
|
||||
|
||||
@@ -713,10 +714,10 @@ class VideoConversion(APIObject):
|
||||
bitRate: int
|
||||
audioTrackId: int
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'VideoConversion.{self.id}')
|
||||
|
||||
|
||||
@@ -727,10 +728,10 @@ class VideoInfo(APIObject):
|
||||
value: str
|
||||
id: str
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(f'VideoInfo.{self.id}')
|
||||
|
||||
|
||||
|
@@ -1,14 +1,13 @@
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
|
||||
from time import sleep
|
||||
from urllib.parse import urlencode
|
||||
from deprecated import deprecated
|
||||
from typing import Optional, Dict, List, Union, cast
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
from deprecated import deprecated
|
||||
|
||||
from .api_objects import (
|
||||
AlbumInfo,
|
||||
@@ -22,6 +21,7 @@ from .api_objects import (
|
||||
Bookmarks,
|
||||
Child,
|
||||
Directory,
|
||||
Error,
|
||||
Genres,
|
||||
Indexes,
|
||||
InternetRadioStations,
|
||||
@@ -38,9 +38,9 @@ from .api_objects import (
|
||||
SearchResult2,
|
||||
SearchResult3,
|
||||
Shares,
|
||||
Songs,
|
||||
Starred,
|
||||
Starred2,
|
||||
Songs,
|
||||
User,
|
||||
Users,
|
||||
VideoInfo,
|
||||
@@ -61,6 +61,10 @@ class Server:
|
||||
* The ``server`` module is stateless. The only thing that it does is allow
|
||||
the module's user to query the \\*sonic server via the API.
|
||||
"""
|
||||
class SubsonicServerError(Exception):
|
||||
def __init__(self: 'Server.SubsonicServerError', error: Error):
|
||||
super().__init__(f'{error.code}: {error.message}')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -77,22 +81,19 @@ class Server:
|
||||
|
||||
def _get_params(self) -> Dict[str, str]:
|
||||
"""See Subsonic API Introduction for details."""
|
||||
return dict(
|
||||
u=self.username,
|
||||
p=self.password,
|
||||
c='Sublime Music',
|
||||
f='json',
|
||||
v='1.15.0',
|
||||
)
|
||||
return {
|
||||
'u': self.username,
|
||||
'p': self.password,
|
||||
'c': 'Sublime Music',
|
||||
'f': 'json',
|
||||
'v': '1.15.0',
|
||||
}
|
||||
|
||||
def _make_url(self, endpoint: str) -> str:
|
||||
return f'{self.hostname}/rest/{endpoint}.view'
|
||||
|
||||
def _subsonic_error_to_exception(self, error) -> Exception:
|
||||
return Exception(f'{error.code}: {error.message}')
|
||||
|
||||
# def _get(self, url, timeout=(3.05, 2), **params):
|
||||
def _get(self, url, **params):
|
||||
def _get(self, url: str, **params) -> Any:
|
||||
params = {**self._get_params(), **params}
|
||||
logging.info(f'[START] get: {url}')
|
||||
|
||||
@@ -105,7 +106,7 @@ class Server:
|
||||
# Deal with datetime parameters (convert to milliseconds since 1970)
|
||||
for k, v in params.items():
|
||||
if type(v) == datetime:
|
||||
params[k] = int(cast(datetime, v).timestamp() * 1000)
|
||||
params[k] = int(v.timestamp() * 1000)
|
||||
|
||||
result = requests.get(
|
||||
url,
|
||||
@@ -151,11 +152,11 @@ class Server:
|
||||
|
||||
# Check for an error and if it exists, raise it.
|
||||
if response.get('error'):
|
||||
raise self._subsonic_error_to_exception(response.error)
|
||||
raise Server.SubsonicServerError(response.error)
|
||||
|
||||
return response
|
||||
|
||||
def do_download(self, url, **params) -> bytes:
|
||||
def do_download(self, url: str, **params) -> bytes:
|
||||
download = self._get(url, **params)
|
||||
if 'json' in download.headers.get('Content-Type'):
|
||||
# TODO (#122): make better
|
||||
@@ -201,7 +202,7 @@ class Server:
|
||||
ifModifiedSince=if_modified_since)
|
||||
return result.indexes
|
||||
|
||||
def get_music_directory(self, dir_id) -> Directory:
|
||||
def get_music_directory(self, dir_id: Union[int, str]) -> Directory:
|
||||
"""
|
||||
Returns a listing of all files in a music directory. Typically used
|
||||
to get list of albums for an artist, or list of songs for an album.
|
||||
@@ -779,17 +780,17 @@ class Server:
|
||||
return self._get_json(self._make_url('deletePlaylist'), id=id)
|
||||
|
||||
def get_stream_url(
|
||||
self,
|
||||
id: str,
|
||||
max_bit_rate: int = None,
|
||||
format: str = None,
|
||||
time_offset: int = None,
|
||||
size: int = None,
|
||||
estimate_content_length: bool = False,
|
||||
converted: bool = False,
|
||||
):
|
||||
self,
|
||||
id: str,
|
||||
max_bit_rate: int = None,
|
||||
format: str = None,
|
||||
time_offset: int = None,
|
||||
size: int = None,
|
||||
estimate_content_length: bool = False,
|
||||
converted: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Gets the URL to streams a given file.
|
||||
Gets the URL to stream a given file.
|
||||
|
||||
:param id: A string which uniquely identifies the file to stream.
|
||||
Obtained by calls to ``getMusicDirectory``.
|
||||
@@ -829,7 +830,7 @@ class Server:
|
||||
params = {k: v for k, v in params.items() if v}
|
||||
return self._make_url('stream') + '?' + urlencode(params)
|
||||
|
||||
def download(self, id: str):
|
||||
def download(self, id: str) -> bytes:
|
||||
"""
|
||||
Downloads a given media file. Similar to stream, but this method
|
||||
returns the original media data without transcoding or downsampling.
|
||||
@@ -839,7 +840,7 @@ class Server:
|
||||
"""
|
||||
return self.do_download(self._make_url('download'), id=id)
|
||||
|
||||
def get_cover_art(self, id: str, size: str = None):
|
||||
def get_cover_art(self, id: str, size: str = None) -> bytes:
|
||||
"""
|
||||
Returns the cover art image in binary form.
|
||||
|
||||
@@ -849,7 +850,7 @@ class Server:
|
||||
return self.do_download(
|
||||
self._make_url('getCoverArt'), id=id, size=size)
|
||||
|
||||
def get_cover_art_url(self, id: str, size: str = None):
|
||||
def get_cover_art_url(self, id: str, size: str = None) -> str:
|
||||
"""
|
||||
Returns the cover art image in binary form.
|
||||
|
||||
@@ -874,7 +875,7 @@ class Server:
|
||||
)
|
||||
return result.lyrics
|
||||
|
||||
def get_avatar(self, username: str):
|
||||
def get_avatar(self, username: str) -> bytes:
|
||||
"""
|
||||
Returns the avatar (personal image) for a user.
|
||||
|
||||
|
@@ -1,17 +1,16 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Set
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
import gi
|
||||
gi.require_version('NetworkManager', '1.0')
|
||||
gi.require_version('NMClient', '1.0')
|
||||
from gi.repository import NetworkManager, NMClient
|
||||
|
||||
from .from_json import from_json
|
||||
from .config import AppConfiguration
|
||||
from .cache_manager import CacheManager
|
||||
from .config import AppConfiguration
|
||||
from .from_json import from_json
|
||||
from .server.api_objects import Child
|
||||
|
||||
|
||||
@@ -21,19 +20,19 @@ class RepeatType(Enum):
|
||||
REPEAT_SONG = 2
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str:
|
||||
icon_name = [
|
||||
'repeat',
|
||||
'repeat-symbolic',
|
||||
'repeat-song-symbolic',
|
||||
][self.value]
|
||||
return 'media-playlist-' + icon_name
|
||||
return f'media-playlist-{icon_name}'
|
||||
|
||||
def as_mpris_loop_status(self):
|
||||
def as_mpris_loop_status(self) -> str:
|
||||
return ['None', 'Playlist', 'Track'][self.value]
|
||||
|
||||
@staticmethod
|
||||
def from_mpris_loop_status(loop_status):
|
||||
def from_mpris_loop_status(loop_status: str) -> 'RepeatType':
|
||||
return {
|
||||
'None': RepeatType.NO_REPEAT,
|
||||
'Track': RepeatType.REPEAT_SONG,
|
||||
@@ -62,7 +61,7 @@ class ApplicationState:
|
||||
current_song_index: int = -1
|
||||
play_queue: List[str] = []
|
||||
old_play_queue: List[str] = []
|
||||
_volume: dict = {'this device': 100}
|
||||
_volume: Dict[str, float] = {'this device': 100.0}
|
||||
is_muted: bool = False
|
||||
repeat_type: RepeatType = RepeatType.NO_REPEAT
|
||||
shuffle_on: bool = False
|
||||
@@ -87,7 +86,7 @@ class ApplicationState:
|
||||
nmclient_initialized = False
|
||||
_current_ssids: Set[str] = set()
|
||||
|
||||
def to_json(self):
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
exclude = ('config', 'repeat_type', '_current_ssids')
|
||||
json_object = {
|
||||
k: getattr(self, k)
|
||||
@@ -101,12 +100,12 @@ class ApplicationState:
|
||||
})
|
||||
return json_object
|
||||
|
||||
def load_from_json(self, json_object):
|
||||
def load_from_json(self, json_object: Dict[str, Any]):
|
||||
self.version = json_object.get('version', 0)
|
||||
self.current_song_index = json_object.get('current_song_index', -1)
|
||||
self.play_queue = json_object.get('play_queue', [])
|
||||
self.old_play_queue = json_object.get('old_play_queue', [])
|
||||
self._volume = json_object.get('_volume', {'this device': 100})
|
||||
self._volume = json_object.get('_volume', {'this device': 100.0})
|
||||
self.is_muted = json_object.get('is_muted', False)
|
||||
self.repeat_type = RepeatType(json_object.get('repeat_type', 0))
|
||||
self.shuffle_on = json_object.get('shuffle_on', False)
|
||||
@@ -183,7 +182,7 @@ class ApplicationState:
|
||||
return AppConfiguration()
|
||||
|
||||
@property
|
||||
def current_ssids(self):
|
||||
def current_ssids(self) -> Set[str]:
|
||||
if not self.nmclient_initialized:
|
||||
# Only look at the active WiFi connections.
|
||||
for ac in self.networkmanager_client.get_active_connections():
|
||||
@@ -200,7 +199,7 @@ class ApplicationState:
|
||||
return self._current_ssids
|
||||
|
||||
@property
|
||||
def state_filename(self):
|
||||
def state_filename(self) -> str:
|
||||
default_cache_location = (
|
||||
os.environ.get('XDG_DATA_HOME')
|
||||
or os.path.expanduser('~/.local/share'))
|
||||
@@ -221,9 +220,9 @@ class ApplicationState:
|
||||
return CacheManager.get_song_details(current_song_id).result()
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
return self._volume.get(self.current_device, 100)
|
||||
def volume(self) -> float:
|
||||
return self._volume.get(self.current_device, 100.0)
|
||||
|
||||
@volume.setter
|
||||
def volume(self, value):
|
||||
def volume(self, value: float):
|
||||
self._volume[self.current_device] = value
|
||||
|
@@ -1,18 +1,16 @@
|
||||
import logging
|
||||
from typing import Any, Callable, Iterable, Optional, Tuple, Union
|
||||
|
||||
import gi
|
||||
from typing import Union
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GObject, GLib, Gio, Pango
|
||||
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.server.api_objects import AlbumWithSongsID3, Child
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import AlbumWithSongs, IconButton, SpinnerImage
|
||||
|
||||
from sublime.server.api_objects import Child, AlbumWithSongsID3
|
||||
|
||||
Album = Union[Child, AlbumWithSongsID3]
|
||||
|
||||
|
||||
@@ -97,7 +95,11 @@ class AlbumsPanel(Gtk.Box):
|
||||
scrolled_window.add(self.grid)
|
||||
self.add(scrolled_window)
|
||||
|
||||
def make_combobox(self, items, on_change):
|
||||
def make_combobox(
|
||||
self,
|
||||
items: Iterable[Tuple[str, str]],
|
||||
on_change: Callable[['AlbumsPanel', Gtk.ComboBox], None],
|
||||
) -> Gtk.ComboBox:
|
||||
store = Gtk.ListStore(str, str)
|
||||
for item in items:
|
||||
store.append(item)
|
||||
@@ -120,7 +122,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
if not CacheManager.ready():
|
||||
return
|
||||
|
||||
def get_genres_done(f):
|
||||
def get_genres_done(f: CacheManager.Result):
|
||||
try:
|
||||
new_store = [
|
||||
(genre.value, genre.value) for genre in (f.result() or [])
|
||||
@@ -150,7 +152,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
self.populate_genre_combo(state, force=force)
|
||||
|
||||
# Show/hide the combo boxes.
|
||||
def show_if(sort_type, *elements):
|
||||
def show_if(sort_type: str, *elements):
|
||||
for element in elements:
|
||||
if state.current_album_sort == sort_type:
|
||||
element.show()
|
||||
@@ -175,15 +177,16 @@ class AlbumsPanel(Gtk.Box):
|
||||
selected_id=state.selected_album_id,
|
||||
)
|
||||
|
||||
def get_id(self, combo):
|
||||
def get_id(self, combo: Gtk.ComboBox) -> Optional[int]:
|
||||
tree_iter = combo.get_active_iter()
|
||||
if tree_iter is not None:
|
||||
return combo.get_model()[tree_iter][0]
|
||||
return None
|
||||
|
||||
def on_refresh_clicked(self, button):
|
||||
def on_refresh_clicked(self, button: Any):
|
||||
self.emit('refresh-window', {}, True)
|
||||
|
||||
def on_type_combo_changed(self, combo):
|
||||
def on_type_combo_changed(self, combo: Gtk.ComboBox):
|
||||
new_active_sort = self.get_id(combo)
|
||||
self.grid.update_params(type_=new_active_sort)
|
||||
self.emit_if_not_updating(
|
||||
@@ -195,7 +198,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
False,
|
||||
)
|
||||
|
||||
def on_alphabetical_type_change(self, combo):
|
||||
def on_alphabetical_type_change(self, combo: Gtk.ComboBox):
|
||||
new_active_alphabetical_sort = self.get_id(combo)
|
||||
self.grid.update_params(alphabetical_type=new_active_alphabetical_sort)
|
||||
self.emit_if_not_updating(
|
||||
@@ -208,7 +211,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
False,
|
||||
)
|
||||
|
||||
def on_genre_change(self, combo):
|
||||
def on_genre_change(self, combo: Gtk.ComboBox):
|
||||
new_active_genre = self.get_id(combo)
|
||||
self.grid.update_params(genre=new_active_genre)
|
||||
self.emit_if_not_updating(
|
||||
@@ -220,7 +223,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
True,
|
||||
)
|
||||
|
||||
def on_year_changed(self, entry):
|
||||
def on_year_changed(self, entry: Gtk.Entry):
|
||||
try:
|
||||
year = int(entry.get_text())
|
||||
except Exception:
|
||||
@@ -250,7 +253,7 @@ class AlbumsPanel(Gtk.Box):
|
||||
True,
|
||||
)
|
||||
|
||||
def on_grid_cover_clicked(self, grid, id):
|
||||
def on_grid_cover_clicked(self, grid: Any, id: str):
|
||||
self.emit(
|
||||
'refresh-window',
|
||||
{'selected_album_id': id},
|
||||
@@ -263,19 +266,6 @@ class AlbumsPanel(Gtk.Box):
|
||||
self.emit(*args)
|
||||
|
||||
|
||||
class AlbumModel(GObject.Object):
|
||||
def __init__(self, album: Album):
|
||||
self.album: Album = album
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.album.id
|
||||
|
||||
def __repr__(self):
|
||||
return f'<AlbumModel {self.album}>'
|
||||
|
||||
|
||||
class AlbumsGrid(Gtk.Overlay):
|
||||
"""Defines the albums panel."""
|
||||
__gsignals__ = {
|
||||
@@ -303,6 +293,18 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
overshoot_update_in_progress = False
|
||||
server_hash = None
|
||||
|
||||
class AlbumModel(GObject.Object):
|
||||
def __init__(self, album: Album):
|
||||
self.album: Album = album
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return self.album.id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<AlbumsGrid.AlbumModel {self.album}>'
|
||||
|
||||
def update_params(
|
||||
self,
|
||||
type_: str = None,
|
||||
@@ -408,12 +410,12 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
|
||||
error_dialog = None
|
||||
|
||||
def update_grid(self, force=False, selected_id=None):
|
||||
def update_grid(self, force: bool = False, selected_id: str = None):
|
||||
if not CacheManager.ready():
|
||||
self.spinner.hide()
|
||||
return
|
||||
|
||||
def reflow_grid(force_reload, selected_index):
|
||||
def reflow_grid(force_reload: bool, selected_index: Optional[int]):
|
||||
selection_changed = (selected_index != self.current_selection)
|
||||
self.current_selection = selected_index
|
||||
self.reflow_grids(
|
||||
@@ -434,7 +436,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
require_reflow = self.parameters_changed
|
||||
self.parameters_changed = False
|
||||
|
||||
def do_update(f):
|
||||
def do_update(f: CacheManager.Result):
|
||||
try:
|
||||
albums = f.result()
|
||||
except Exception as e:
|
||||
@@ -461,7 +463,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
|
||||
selected_index = None
|
||||
for i, album in enumerate(albums):
|
||||
model = AlbumModel(album)
|
||||
model = AlbumsGrid.AlbumModel(album)
|
||||
|
||||
if model.id == selected_id:
|
||||
selected_index = i
|
||||
@@ -485,7 +487,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
|
||||
# Event Handlers
|
||||
# =========================================================================
|
||||
def on_child_activated(self, flowbox, child):
|
||||
def on_child_activated(self, flowbox: Gtk.FlowBox, child: Gtk.Widget):
|
||||
click_top = flowbox == self.grid_top
|
||||
selected_index = (
|
||||
child.get_index() + (0 if click_top else len(self.list_store_top)))
|
||||
@@ -495,7 +497,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
else:
|
||||
self.emit('cover-clicked', self.list_store[selected_index].id)
|
||||
|
||||
def on_grid_resize(self, flowbox, rect):
|
||||
def on_grid_resize(self, flowbox: Gtk.FlowBox, rect: Gdk.Rectangle):
|
||||
# TODO (#124): this doesn't work with themes that add extra padding.
|
||||
# 200 + (10 * 2) + (5 * 2) = 230
|
||||
# picture + (padding * 2) + (margin * 2)
|
||||
@@ -511,7 +513,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
|
||||
# Helper Methods
|
||||
# =========================================================================
|
||||
def create_widget(self, item):
|
||||
def create_widget(self, item: 'AlbumsGrid.AlbumModel') -> Gtk.Box:
|
||||
widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
# Cover art image
|
||||
@@ -523,7 +525,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
)
|
||||
widget_box.pack_start(artwork, False, False, 0)
|
||||
|
||||
def make_label(text, name):
|
||||
def make_label(text: str, name: str) -> Gtk.Label:
|
||||
return Gtk.Label(
|
||||
name=name,
|
||||
label=text,
|
||||
@@ -535,7 +537,8 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
|
||||
# Header for the widget
|
||||
header_text = (
|
||||
item.album.title if type(item.album) == Child else item.album.name)
|
||||
item.album.title
|
||||
if isinstance(item.album, Child) else item.album.name)
|
||||
|
||||
header_label = make_label(header_text, 'grid-header-label')
|
||||
widget_box.pack_start(header_label, False, False, 0)
|
||||
@@ -547,7 +550,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
widget_box.pack_start(info_label, False, False, 0)
|
||||
|
||||
# Download the cover art.
|
||||
def on_artwork_downloaded(f):
|
||||
def on_artwork_downloaded(f: CacheManager.Result):
|
||||
artwork.set_from_file(f.result())
|
||||
artwork.set_loading(False)
|
||||
|
||||
@@ -566,8 +569,8 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
|
||||
def reflow_grids(
|
||||
self,
|
||||
force_reload_from_master=False,
|
||||
selection_changed=False,
|
||||
force_reload_from_master: bool = False,
|
||||
selection_changed: bool = False,
|
||||
):
|
||||
# Determine where the cuttoff is between the top and bottom grids.
|
||||
entries_before_fold = len(self.list_store)
|
||||
|
@@ -1,22 +1,21 @@
|
||||
from typing import cast, List, Union
|
||||
from random import randint
|
||||
from typing import Any, cast, List, Optional, Union
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GObject, Pango, GLib, Gio
|
||||
from gi.repository import Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import AlbumWithSongs, IconButton, SpinnerImage
|
||||
|
||||
from sublime.server.api_objects import (
|
||||
AlbumID3,
|
||||
ArtistID3,
|
||||
ArtistInfo2,
|
||||
ArtistWithAlbumsID3,
|
||||
Child,
|
||||
)
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import AlbumWithSongs, IconButton, SpinnerImage
|
||||
|
||||
|
||||
class ArtistsPanel(Gtk.Paned):
|
||||
@@ -48,23 +47,24 @@ class ArtistsPanel(Gtk.Paned):
|
||||
)
|
||||
self.pack2(self.artist_detail_panel, True, False)
|
||||
|
||||
def update(self, state: ApplicationState, force=False):
|
||||
def update(self, state: ApplicationState, force: bool = False):
|
||||
self.artist_list.update(state=state)
|
||||
self.artist_detail_panel.update(state=state)
|
||||
|
||||
|
||||
class _ArtistModel(GObject.GObject):
|
||||
artist_id = GObject.Property(type=str)
|
||||
name = GObject.Property(type=str)
|
||||
album_count = GObject.Property(type=int)
|
||||
|
||||
def __init__(self, artist_id: str, name: str, album_count: int):
|
||||
GObject.GObject.__init__(self)
|
||||
self.artist_id = artist_id
|
||||
self.name = name
|
||||
self.album_count = album_count
|
||||
|
||||
|
||||
class ArtistList(Gtk.Box):
|
||||
class ArtistModel(GObject.GObject):
|
||||
artist_id = GObject.Property(type=str)
|
||||
name = GObject.Property(type=str)
|
||||
album_count = GObject.Property(type=int)
|
||||
|
||||
def __init__(self, artist_id, name, album_count):
|
||||
GObject.GObject.__init__(self)
|
||||
self.artist_id = artist_id
|
||||
self.name = name
|
||||
self.album_count = album_count
|
||||
|
||||
def __init__(self):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
@@ -85,7 +85,7 @@ class ArtistList(Gtk.Box):
|
||||
|
||||
list_scroll_window = Gtk.ScrolledWindow(min_content_width=250)
|
||||
|
||||
def create_artist_row(model: ArtistList.ArtistModel):
|
||||
def create_artist_row(model: _ArtistModel) -> Gtk.ListBoxRow:
|
||||
label_text = [f'<b>{util.esc(model.name)}</b>']
|
||||
|
||||
album_count = model.album_count
|
||||
@@ -122,7 +122,12 @@ class ArtistList(Gtk.Box):
|
||||
before_download=lambda self: self.loading_indicator.show_all(),
|
||||
on_failure=lambda self, e: self.loading_indicator.hide(),
|
||||
)
|
||||
def update(self, artists, state: ApplicationState, **kwargs):
|
||||
def update(
|
||||
self,
|
||||
artists: List[ArtistID3],
|
||||
state: ApplicationState,
|
||||
**kwargs,
|
||||
):
|
||||
new_store = []
|
||||
selected_idx = None
|
||||
for i, artist in enumerate(artists):
|
||||
@@ -130,7 +135,7 @@ class ArtistList(Gtk.Box):
|
||||
selected_idx = i
|
||||
|
||||
new_store.append(
|
||||
ArtistList.ArtistModel(
|
||||
_ArtistModel(
|
||||
artist.id,
|
||||
artist.name,
|
||||
artist.get('albumCount', ''),
|
||||
@@ -299,8 +304,8 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
self,
|
||||
artist: ArtistWithAlbumsID3,
|
||||
state: ApplicationState,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
@@ -330,8 +335,8 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
self,
|
||||
artist_info: ArtistInfo2,
|
||||
state: ApplicationState,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
@@ -363,10 +368,10 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
)
|
||||
def update_artist_artwork(
|
||||
self,
|
||||
cover_art_filename,
|
||||
cover_art_filename: str,
|
||||
state: ApplicationState,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
@@ -383,9 +388,9 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
order_token=self.update_order_token,
|
||||
)
|
||||
|
||||
def on_download_all_click(self, btn):
|
||||
def on_download_all_click(self, btn: Any):
|
||||
CacheManager.batch_download_songs(
|
||||
self.get_artist_songs(),
|
||||
self.get_artist_song_ids(),
|
||||
before_download=lambda: self.update_artist_view(
|
||||
self.artist_id,
|
||||
order_token=self.update_order_token,
|
||||
@@ -396,8 +401,8 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
),
|
||||
)
|
||||
|
||||
def on_play_all_clicked(self, btn):
|
||||
songs = self.get_artist_songs()
|
||||
def on_play_all_clicked(self, btn: Any):
|
||||
songs = self.get_artist_song_ids()
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
0,
|
||||
@@ -405,8 +410,8 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
{'force_shuffle_state': False},
|
||||
)
|
||||
|
||||
def on_shuffle_all_button(self, btn):
|
||||
songs = self.get_artist_songs()
|
||||
def on_shuffle_all_button(self, btn: Any):
|
||||
songs = self.get_artist_song_ids()
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
randint(0,
|
||||
@@ -417,7 +422,7 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
|
||||
# Helper Methods
|
||||
# =========================================================================
|
||||
def set_all_loading(self, loading_state):
|
||||
def set_all_loading(self, loading_state: bool):
|
||||
if loading_state:
|
||||
self.albums_list.spinner.start()
|
||||
self.albums_list.spinner.show()
|
||||
@@ -426,7 +431,12 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
self.albums_list.spinner.hide()
|
||||
self.artist_artwork.set_loading(False)
|
||||
|
||||
def make_label(self, text=None, name=None, **params):
|
||||
def make_label(
|
||||
self,
|
||||
text: str = None,
|
||||
name: str = None,
|
||||
**params,
|
||||
) -> Gtk.Label:
|
||||
return Gtk.Label(
|
||||
label=text,
|
||||
name=name,
|
||||
@@ -435,34 +445,21 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
**params,
|
||||
)
|
||||
|
||||
def format_stats(self, artist):
|
||||
album_count = artist.get('albumCount', len(artist.get('child') or []))
|
||||
components = [
|
||||
def format_stats(self, artist: ArtistWithAlbumsID3) -> str:
|
||||
album_count = artist.get('albumCount', 0)
|
||||
song_count = sum(a.songCount for a in artist.album)
|
||||
duration = sum(a.duration for a in artist.album)
|
||||
return util.dot_join(
|
||||
'{} {}'.format(album_count, util.pluralize('album', album_count)),
|
||||
]
|
||||
'{} {}'.format(song_count, util.pluralize('song', song_count)),
|
||||
util.format_sequence_duration(duration),
|
||||
)
|
||||
|
||||
if artist.get('album'):
|
||||
song_count = sum(a.songCount for a in artist.album)
|
||||
duration = sum(a.duration for a in artist.album)
|
||||
components += [
|
||||
'{} {}'.format(song_count, util.pluralize('song', song_count)),
|
||||
util.format_sequence_duration(duration),
|
||||
]
|
||||
elif artist.get('child'):
|
||||
plays = sum(c.playCount for c in artist.child)
|
||||
components += [
|
||||
'{} {}'.format(plays, util.pluralize('play', plays)),
|
||||
]
|
||||
|
||||
return util.dot_join(*components)
|
||||
|
||||
def get_artist_songs(self):
|
||||
def get_artist_song_ids(self) -> List[int]:
|
||||
songs = []
|
||||
artist = CacheManager.get_artist(self.artist_id).result()
|
||||
for album in artist.get('album', artist.get('child', [])):
|
||||
for album in CacheManager.get_artist(self.artist_id).result().album:
|
||||
album_songs = CacheManager.get_album(album.id).result()
|
||||
album_songs = album_songs.get('child', album_songs.get('song', []))
|
||||
for song in album_songs:
|
||||
for song in album_songs.get('song', []):
|
||||
songs.append(song.id)
|
||||
|
||||
return songs
|
||||
@@ -494,7 +491,7 @@ class AlbumsListWithSongs(Gtk.Overlay):
|
||||
|
||||
self.albums = []
|
||||
|
||||
def update(self, artist):
|
||||
def update(self, artist: ArtistWithAlbumsID3):
|
||||
def remove_all():
|
||||
for c in self.box.get_children():
|
||||
self.box.remove(c)
|
||||
@@ -528,7 +525,7 @@ class AlbumsListWithSongs(Gtk.Overlay):
|
||||
self.spinner.stop()
|
||||
self.spinner.hide()
|
||||
|
||||
def on_song_selected(self, album_component):
|
||||
def on_song_selected(self, album_component: AlbumWithSongs):
|
||||
for child in self.box.get_children():
|
||||
if album_component != child:
|
||||
child.deselect_all()
|
||||
|
@@ -1,17 +1,15 @@
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GObject, Pango, GLib, Gio
|
||||
from gi.repository import Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.server.api_objects import Artist, Child
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import IconButton
|
||||
|
||||
from sublime.server.api_objects import Child, Artist
|
||||
|
||||
|
||||
class BrowsePanel(Gtk.Overlay):
|
||||
"""Defines the arist panel."""
|
||||
|
@@ -1,22 +1,16 @@
|
||||
from typing import Union
|
||||
from random import randint
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GObject, Pango, GLib
|
||||
from gi.repository import Gdk, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.server.api_objects import AlbumWithSongsID3, Child, Directory
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.ui import util
|
||||
from .icon_button import IconButton
|
||||
from .spinner_image import SpinnerImage
|
||||
|
||||
from sublime.server.api_objects import (
|
||||
AlbumWithSongsID3,
|
||||
Child,
|
||||
Directory,
|
||||
)
|
||||
from sublime.ui.common.icon_button import IconButton
|
||||
from sublime.ui.common.spinner_image import SpinnerImage
|
||||
|
||||
|
||||
class AlbumWithSongs(Gtk.Box):
|
||||
@@ -33,7 +27,12 @@ class AlbumWithSongs(Gtk.Box):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, album, cover_art_size=200, show_artist_name=True):
|
||||
def __init__(
|
||||
self,
|
||||
album: AlbumWithSongsID3,
|
||||
cover_art_size: int = 200,
|
||||
show_artist_name: bool = True,
|
||||
):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.album = album
|
||||
|
||||
@@ -51,7 +50,7 @@ class AlbumWithSongs(Gtk.Box):
|
||||
box.pack_start(Gtk.Box(), True, True, 0)
|
||||
self.pack_start(box, False, False, 0)
|
||||
|
||||
def cover_art_future_done(f):
|
||||
def cover_art_future_done(f: CacheManager.Result):
|
||||
artist_artwork.set_from_file(f.result())
|
||||
artist_artwork.set_loading(False)
|
||||
|
||||
@@ -125,7 +124,13 @@ class AlbumWithSongs(Gtk.Box):
|
||||
str, # song ID
|
||||
)
|
||||
|
||||
def create_column(header, text_idx, bold=False, align=0, width=None):
|
||||
def create_column(
|
||||
header: str,
|
||||
text_idx: int,
|
||||
bold: bool = False,
|
||||
align: int = 0,
|
||||
width: Optional[int] = None,
|
||||
) -> Gtk.TreeViewColumn:
|
||||
renderer = Gtk.CellRendererText(
|
||||
xalign=align,
|
||||
weight=Pango.Weight.BOLD if bold else Pango.Weight.NORMAL,
|
||||
@@ -177,11 +182,11 @@ class AlbumWithSongs(Gtk.Box):
|
||||
|
||||
# Event Handlers
|
||||
# =========================================================================
|
||||
def on_song_selection_change(self, event):
|
||||
def on_song_selection_change(self, event: Any):
|
||||
if not self.album_songs.has_focus():
|
||||
self.emit('song-selected')
|
||||
|
||||
def on_song_activated(self, treeview, idx, column):
|
||||
def on_song_activated(self, treeview: Any, idx: Gtk.TreePath, column: Any):
|
||||
# The song ID is in the last column of the model.
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
@@ -190,7 +195,7 @@ class AlbumWithSongs(Gtk.Box):
|
||||
{},
|
||||
)
|
||||
|
||||
def on_song_button_press(self, tree, event):
|
||||
def on_song_button_press(self, tree: Any, event: Gdk.EventButton) -> bool:
|
||||
if event.button == 3: # Right click
|
||||
clicked_path = tree.get_path_at_pos(event.x, event.y)
|
||||
if not clicked_path:
|
||||
@@ -199,7 +204,7 @@ class AlbumWithSongs(Gtk.Box):
|
||||
store, paths = tree.get_selection().get_selected_rows()
|
||||
allow_deselect = False
|
||||
|
||||
def on_download_state_change(song_id=None):
|
||||
def on_download_state_change(song_id: Any = None):
|
||||
self.update_album_songs(self.album.id)
|
||||
|
||||
# Use the new selection instead of the old one for calculating what
|
||||
@@ -228,14 +233,16 @@ class AlbumWithSongs(Gtk.Box):
|
||||
if not allow_deselect:
|
||||
return True
|
||||
|
||||
def on_download_all_click(self, btn):
|
||||
return False
|
||||
|
||||
def on_download_all_click(self, btn: Any):
|
||||
CacheManager.batch_download_songs(
|
||||
[x[-1] for x in self.album_song_store],
|
||||
before_download=self.update,
|
||||
on_song_download_complete=lambda x: self.update(),
|
||||
)
|
||||
|
||||
def play_btn_clicked(self, btn):
|
||||
def play_btn_clicked(self, btn: Any):
|
||||
song_ids = [x[-1] for x in self.album_song_store]
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
@@ -244,7 +251,7 @@ class AlbumWithSongs(Gtk.Box):
|
||||
{'force_shuffle_state': False},
|
||||
)
|
||||
|
||||
def shuffle_btn_clicked(self, btn):
|
||||
def shuffle_btn_clicked(self, btn: Any):
|
||||
song_ids = [x[-1] for x in self.album_song_store]
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
@@ -259,10 +266,10 @@ class AlbumWithSongs(Gtk.Box):
|
||||
def deselect_all(self):
|
||||
self.album_songs.get_selection().unselect_all()
|
||||
|
||||
def update(self, force=False):
|
||||
def update(self, force: bool = False):
|
||||
self.update_album_songs(self.album.id)
|
||||
|
||||
def set_loading(self, loading):
|
||||
def set_loading(self, loading: bool):
|
||||
if loading:
|
||||
self.loading_indicator.start()
|
||||
self.loading_indicator.show()
|
||||
@@ -279,8 +286,8 @@ class AlbumWithSongs(Gtk.Box):
|
||||
self,
|
||||
album: Union[AlbumWithSongsID3, Child, Directory],
|
||||
state: ApplicationState,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
):
|
||||
new_store = [
|
||||
[
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import List, Tuple, Optional
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
@@ -17,7 +17,7 @@ class EditFormDialog(Gtk.Dialog):
|
||||
extra_label: Optional[str] = None
|
||||
extra_buttons: List[Gtk.Button] = []
|
||||
|
||||
def get_object_name(self, obj):
|
||||
def get_object_name(self, obj: Any) -> str:
|
||||
"""
|
||||
Gets the friendly object name. Can be overridden.
|
||||
"""
|
||||
@@ -26,7 +26,7 @@ class EditFormDialog(Gtk.Dialog):
|
||||
def get_default_object(self):
|
||||
return None
|
||||
|
||||
def __init__(self, parent, existing_object=None):
|
||||
def __init__(self, parent: Any, existing_object: Any = None):
|
||||
editing = existing_object is not None
|
||||
title = getattr(self, 'title', lambda: None)
|
||||
if not title:
|
||||
|
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
@@ -5,12 +7,12 @@ from gi.repository import Gtk
|
||||
|
||||
class IconButton(Gtk.Button):
|
||||
def __init__(
|
||||
self,
|
||||
icon_name,
|
||||
relief=False,
|
||||
icon_size=Gtk.IconSize.BUTTON,
|
||||
label=None,
|
||||
**kwargs,
|
||||
self,
|
||||
icon_name: Optional[str],
|
||||
relief: bool = False,
|
||||
icon_size: Gtk.IconSize = Gtk.IconSize.BUTTON,
|
||||
label: str = None,
|
||||
**kwargs,
|
||||
):
|
||||
Gtk.Button.__init__(self, **kwargs)
|
||||
self.icon_size = icon_size
|
||||
@@ -29,5 +31,5 @@ class IconButton(Gtk.Button):
|
||||
|
||||
self.add(box)
|
||||
|
||||
def set_icon(self, icon_name):
|
||||
def set_icon(self, icon_name: str):
|
||||
self.image.set_from_icon_name(icon_name, self.icon_size)
|
||||
|
@@ -1,16 +1,18 @@
|
||||
from typing import Optional
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GdkPixbuf
|
||||
from gi.repository import GdkPixbuf, Gtk
|
||||
|
||||
|
||||
class SpinnerImage(Gtk.Overlay):
|
||||
def __init__(
|
||||
self,
|
||||
loading=True,
|
||||
image_name=None,
|
||||
spinner_name=None,
|
||||
image_size=None,
|
||||
**kwargs,
|
||||
self,
|
||||
loading: bool = True,
|
||||
image_name: str = None,
|
||||
spinner_name: str = None,
|
||||
image_size: int = None,
|
||||
**kwargs,
|
||||
):
|
||||
Gtk.Overlay.__init__(self)
|
||||
self.image_size = image_size
|
||||
@@ -26,7 +28,7 @@ class SpinnerImage(Gtk.Overlay):
|
||||
)
|
||||
self.add_overlay(self.spinner)
|
||||
|
||||
def set_from_file(self, filename):
|
||||
def set_from_file(self, filename: Optional[str]):
|
||||
if filename == '':
|
||||
filename = None
|
||||
if self.image_size is not None and filename:
|
||||
@@ -40,7 +42,7 @@ class SpinnerImage(Gtk.Overlay):
|
||||
else:
|
||||
self.image.set_from_file(filename)
|
||||
|
||||
def set_loading(self, loading_status):
|
||||
def set_loading(self, loading_status: bool):
|
||||
if loading_status:
|
||||
self.spinner.start()
|
||||
self.spinner.show()
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GObject
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
from sublime.config import AppConfiguration, ServerConfiguration
|
||||
from sublime.server import Server
|
||||
from sublime.config import ServerConfiguration
|
||||
from sublime.ui.common import EditFormDialog, IconButton
|
||||
|
||||
|
||||
@@ -36,7 +37,7 @@ class EditServerDialog(EditFormDialog):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def on_test_server_clicked(self, event):
|
||||
def on_test_server_clicked(self, event: Any):
|
||||
# Instantiate the server.
|
||||
server_address = self.data['server_address'].get_text()
|
||||
server = Server(
|
||||
@@ -72,7 +73,7 @@ class EditServerDialog(EditFormDialog):
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def on_open_in_browser_clicked(self, event):
|
||||
def on_open_in_browser_clicked(self, event: Any):
|
||||
subprocess.call(['xdg-open', self.data['server_address'].get_text()])
|
||||
|
||||
|
||||
@@ -84,7 +85,7 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
(GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object, )),
|
||||
}
|
||||
|
||||
def __init__(self, parent, config):
|
||||
def __init__(self, parent: Any, config: AppConfiguration):
|
||||
Gtk.Dialog.__init__(
|
||||
self,
|
||||
title='Configure Servers',
|
||||
@@ -116,13 +117,13 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
'document-edit-symbolic',
|
||||
label='Edit...',
|
||||
relief=True,
|
||||
), lambda e: self.on_edit_clicked(e, False), 'start', True),
|
||||
), lambda e: self.on_edit_clicked(False), 'start', True),
|
||||
(
|
||||
IconButton(
|
||||
'list-add-symbolic',
|
||||
label='Add...',
|
||||
relief=True,
|
||||
), lambda e: self.on_edit_clicked(e, True), 'start', False),
|
||||
), lambda e: self.on_edit_clicked(True), 'start', False),
|
||||
(
|
||||
IconButton(
|
||||
'list-remove-symbolic',
|
||||
@@ -191,14 +192,14 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
self.server_list.select_row(
|
||||
self.server_list.get_row_at_index(self.selected_server_index))
|
||||
|
||||
def on_remove_clicked(self, event):
|
||||
def on_remove_clicked(self, event: Any):
|
||||
selected = self.server_list.get_selected_row()
|
||||
if selected:
|
||||
del self.server_configs[selected.get_index()]
|
||||
self.refresh_server_list()
|
||||
self.emit('server-list-changed', self.server_configs)
|
||||
|
||||
def on_edit_clicked(self, event, add):
|
||||
def on_edit_clicked(self, add: bool):
|
||||
if add:
|
||||
dialog = EditServerDialog(self)
|
||||
else:
|
||||
@@ -236,12 +237,12 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
def on_server_list_activate(self, *args):
|
||||
self.on_connect_clicked(None)
|
||||
|
||||
def on_connect_clicked(self, event):
|
||||
def on_connect_clicked(self, event: Any):
|
||||
selected_index = self.server_list.get_selected_row().get_index()
|
||||
self.emit('connected-server-changed', selected_index)
|
||||
self.close()
|
||||
|
||||
def server_list_on_selected_rows_changed(self, event):
|
||||
def server_list_on_selected_rows_changed(self, event: Any):
|
||||
# Update the state of the buttons depending on whether or not a row is
|
||||
# selected in the server list.
|
||||
has_selection = self.server_list.get_selected_row()
|
||||
|
@@ -1,15 +1,14 @@
|
||||
from datetime import datetime
|
||||
from typing import Set
|
||||
from typing import Any, Callable, Set
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gio, Gtk, GObject, Gdk, GLib, Pango
|
||||
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from . import albums, artists, browse, playlists, player_controls
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.cache_manager import CacheManager, SearchResult
|
||||
from sublime.server.api_objects import Child
|
||||
from sublime.ui import util
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.ui import (
|
||||
albums, artists, browse, player_controls, playlists, util)
|
||||
from sublime.ui.common import SpinnerImage
|
||||
|
||||
|
||||
@@ -43,7 +42,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
self.set_default_size(1150, 768)
|
||||
|
||||
# Create the stack
|
||||
self.stack = self.create_stack(
|
||||
self.stack = self._create_stack(
|
||||
Albums=albums.AlbumsPanel(),
|
||||
Artists=artists.ArtistsPanel(),
|
||||
Browse=browse.BrowsePanel(),
|
||||
@@ -52,7 +51,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
self.stack.set_transition_type(
|
||||
Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||
|
||||
self.titlebar = self.create_headerbar(self.stack)
|
||||
self.titlebar = self._create_headerbar(self.stack)
|
||||
self.set_titlebar(self.titlebar)
|
||||
|
||||
self.player_controls = player_controls.PlayerControls()
|
||||
@@ -70,9 +69,9 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
flowbox.pack_start(self.player_controls, False, True, 0)
|
||||
self.add(flowbox)
|
||||
|
||||
self.connect('button-release-event', self.on_button_release)
|
||||
self.connect('button-release-event', self._on_button_release)
|
||||
|
||||
def update(self, state: ApplicationState, force=False):
|
||||
def update(self, state: ApplicationState, force: bool = False):
|
||||
# Update the Connected to label on the popup menu.
|
||||
if state.config.current_server >= 0:
|
||||
server_name = state.config.servers[
|
||||
@@ -91,7 +90,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
self.player_controls.update(state)
|
||||
|
||||
def create_stack(self, **kwargs):
|
||||
def _create_stack(self, **kwargs: Gtk.Widget) -> Gtk.Stack:
|
||||
stack = Gtk.Stack()
|
||||
for name, child in kwargs.items():
|
||||
child.connect(
|
||||
@@ -105,7 +104,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
stack.add_titled(child, name.lower(), name)
|
||||
return stack
|
||||
|
||||
def create_headerbar(self, stack):
|
||||
def _create_headerbar(self, stack: Gtk.Stack) -> Gtk.HeaderBar:
|
||||
"""
|
||||
Configure the header bar for the window.
|
||||
"""
|
||||
@@ -116,18 +115,19 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
# Search
|
||||
self.search_entry = Gtk.SearchEntry(
|
||||
placeholder_text='Search everything...')
|
||||
self.search_entry.connect('focus-in-event', self.on_search_entry_focus)
|
||||
self.search_entry.connect(
|
||||
'button-press-event', self.on_search_entry_button_press)
|
||||
'focus-in-event', self._on_search_entry_focus)
|
||||
self.search_entry.connect(
|
||||
'focus-out-event', self.on_search_entry_loose_focus)
|
||||
self.search_entry.connect('changed', self.on_search_entry_changed)
|
||||
'button-press-event', self._on_search_entry_button_press)
|
||||
self.search_entry.connect(
|
||||
'stop-search', self.on_search_entry_stop_search)
|
||||
'focus-out-event', self._on_search_entry_loose_focus)
|
||||
self.search_entry.connect('changed', self._on_search_entry_changed)
|
||||
self.search_entry.connect(
|
||||
'stop-search', self._on_search_entry_stop_search)
|
||||
header.pack_start(self.search_entry)
|
||||
|
||||
# Search popup
|
||||
self.create_search_popup()
|
||||
self._create_search_popup()
|
||||
|
||||
# Stack switcher
|
||||
switcher = Gtk.StackSwitcher(stack=stack)
|
||||
@@ -136,8 +136,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
# Menu button
|
||||
menu_button = Gtk.MenuButton()
|
||||
menu_button.set_use_popover(True)
|
||||
menu_button.set_popover(self.create_menu())
|
||||
menu_button.connect('clicked', self.on_menu_clicked)
|
||||
menu_button.set_popover(self._create_menu())
|
||||
menu_button.connect('clicked', self._on_menu_clicked)
|
||||
self.menu.set_relative_to(menu_button)
|
||||
|
||||
icon = Gio.ThemedIcon(name='open-menu-symbolic')
|
||||
@@ -148,7 +148,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
return header
|
||||
|
||||
def create_label(self, text, *args, **kwargs):
|
||||
def _create_label(self, text: str, *args, **kwargs) -> Gtk.Label:
|
||||
label = Gtk.Label(
|
||||
use_markup=True,
|
||||
halign=Gtk.Align.START,
|
||||
@@ -160,10 +160,10 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
label.get_style_context().add_class('search-result-row')
|
||||
return label
|
||||
|
||||
def create_menu(self):
|
||||
def _create_menu(self) -> Gtk.PopoverMenu:
|
||||
self.menu = Gtk.PopoverMenu()
|
||||
|
||||
self.connected_to_label = self.create_label(
|
||||
self.connected_to_label = self._create_label(
|
||||
'', name='connected-to-label')
|
||||
self.connected_to_label.set_markup(
|
||||
f'<span style="italic">Not Connected to a Server</span>')
|
||||
@@ -187,7 +187,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
return self.menu
|
||||
|
||||
def create_search_popup(self):
|
||||
def _create_search_popup(self) -> Gtk.PopoverMenu:
|
||||
self.search_popup = Gtk.PopoverMenu(modal=False)
|
||||
|
||||
results_scrollbox = Gtk.ScrolledWindow(
|
||||
@@ -195,8 +195,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
min_content_height=750,
|
||||
)
|
||||
|
||||
def make_search_result_header(text):
|
||||
label = self.create_label(text)
|
||||
def make_search_result_header(text: str) -> Gtk.Label:
|
||||
label = self._create_label(text)
|
||||
label.get_style_context().add_class('search-result-header')
|
||||
return label
|
||||
|
||||
@@ -238,22 +238,22 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
# Event Listeners
|
||||
# =========================================================================
|
||||
def on_button_release(self, win, event):
|
||||
if not self.event_in_widgets(
|
||||
def _on_button_release(self, win: Any, event: Gdk.EventButton) -> bool:
|
||||
if not self._event_in_widgets(
|
||||
event,
|
||||
self.search_entry,
|
||||
self.search_popup,
|
||||
):
|
||||
self.hide_search()
|
||||
self._hide_search()
|
||||
|
||||
if not self.event_in_widgets(
|
||||
if not self._event_in_widgets(
|
||||
event,
|
||||
self.player_controls.device_button,
|
||||
self.player_controls.device_popover,
|
||||
):
|
||||
self.player_controls.device_popover.popdown()
|
||||
|
||||
if not self.event_in_widgets(
|
||||
if not self._event_in_widgets(
|
||||
event,
|
||||
self.player_controls.play_queue_button,
|
||||
self.player_controls.play_queue_popover,
|
||||
@@ -262,25 +262,25 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
return False
|
||||
|
||||
def on_menu_clicked(self, button):
|
||||
def _on_menu_clicked(self, *args):
|
||||
self.menu.popup()
|
||||
self.menu.show_all()
|
||||
|
||||
def on_search_entry_focus(self, entry, event):
|
||||
self.show_search()
|
||||
def _on_search_entry_focus(self, *args):
|
||||
self._show_search()
|
||||
|
||||
def on_search_entry_button_press(self, *args):
|
||||
self.show_search()
|
||||
def _on_search_entry_button_press(self, *args):
|
||||
self._show_search()
|
||||
|
||||
def on_search_entry_loose_focus(self, entry, event):
|
||||
self.hide_search()
|
||||
def _on_search_entry_loose_focus(self, *args):
|
||||
self._hide_search()
|
||||
|
||||
search_idx = 0
|
||||
latest_returned_search_idx = 0
|
||||
last_search_change_time = datetime.now()
|
||||
searches: Set[SearchResult] = set()
|
||||
searches: Set[CacheManager.Result] = set()
|
||||
|
||||
def on_search_entry_changed(self, entry):
|
||||
def _on_search_entry_changed(self, entry: Gtk.Entry):
|
||||
now = datetime.now()
|
||||
if (now - self.last_search_change_time).seconds < 0.5:
|
||||
while len(self.searches) > 0:
|
||||
@@ -293,8 +293,11 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
self.search_popup.show_all()
|
||||
self.search_popup.popup()
|
||||
|
||||
def create_search_callback(idx):
|
||||
def search_result_calback(result, is_last_in_batch):
|
||||
def create_search_callback(idx: int) -> Callable[..., Any]:
|
||||
def search_result_calback(
|
||||
result: SearchResult,
|
||||
is_last_in_batch: bool,
|
||||
):
|
||||
# Ignore slow returned searches.
|
||||
if idx < self.latest_returned_search_idx:
|
||||
return
|
||||
@@ -302,10 +305,10 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
# If all results are back, the stop the loading indicator.
|
||||
if is_last_in_batch:
|
||||
if idx == self.search_idx - 1:
|
||||
self.set_search_loading(False)
|
||||
self._set_search_loading(False)
|
||||
self.latest_returned_search_idx = idx
|
||||
|
||||
self.update_search_results(result)
|
||||
self._update_search_results(result)
|
||||
|
||||
return lambda *a: GLib.idle_add(search_result_calback, *a)
|
||||
|
||||
@@ -313,27 +316,27 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
CacheManager.search(
|
||||
entry.get_text(),
|
||||
search_callback=create_search_callback(self.search_idx),
|
||||
before_download=lambda: self.set_search_loading(True),
|
||||
before_download=lambda: self._set_search_loading(True),
|
||||
))
|
||||
|
||||
self.search_idx += 1
|
||||
|
||||
def on_search_entry_stop_search(self, entry):
|
||||
def _on_search_entry_stop_search(self, entry: Any):
|
||||
self.search_popup.popdown()
|
||||
|
||||
# Helper Functions
|
||||
# =========================================================================
|
||||
def show_search(self):
|
||||
def _show_search(self):
|
||||
self.search_entry.set_size_request(300, -1)
|
||||
self.search_popup.show_all()
|
||||
self.search_results_loading.hide()
|
||||
self.search_popup.popup()
|
||||
|
||||
def hide_search(self):
|
||||
def _hide_search(self):
|
||||
self.search_popup.popdown()
|
||||
self.search_entry.set_size_request(-1, -1)
|
||||
|
||||
def set_search_loading(self, loading_state):
|
||||
def _set_search_loading(self, loading_state: bool):
|
||||
if loading_state:
|
||||
self.search_results_loading.start()
|
||||
self.search_results_loading.show_all()
|
||||
@@ -341,24 +344,24 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
self.search_results_loading.stop()
|
||||
self.search_results_loading.hide()
|
||||
|
||||
def remove_all_from_widget(self, widget):
|
||||
def _remove_all_from_widget(self, widget: Gtk.Widget):
|
||||
for c in widget.get_children():
|
||||
widget.remove(c)
|
||||
|
||||
def create_search_result_row(
|
||||
self,
|
||||
text,
|
||||
action_name,
|
||||
value,
|
||||
artwork_future,
|
||||
):
|
||||
def on_search_row_button_press(btn, event):
|
||||
def _create_search_result_row(
|
||||
self,
|
||||
text: str,
|
||||
action_name: str,
|
||||
value: Any,
|
||||
artwork_future: CacheManager.Result,
|
||||
) -> Gtk.Button:
|
||||
def on_search_row_button_press(*args):
|
||||
if action_name == 'song':
|
||||
goto_action_name, goto_id = 'album', value.albumId
|
||||
else:
|
||||
goto_action_name, goto_id = action_name, value.id
|
||||
self.emit('go-to', goto_action_name, goto_id)
|
||||
self.hide_search()
|
||||
self._hide_search()
|
||||
|
||||
row = Gtk.Button(relief=Gtk.ReliefStyle.NONE)
|
||||
row.connect('button-press-event', on_search_row_button_press)
|
||||
@@ -366,10 +369,10 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
image = SpinnerImage(image_name='search-artwork', image_size=30)
|
||||
box.add(image)
|
||||
box.add(self.create_label(text))
|
||||
box.add(self._create_label(text))
|
||||
row.add(box)
|
||||
|
||||
def image_callback(f):
|
||||
def image_callback(f: CacheManager.Result):
|
||||
image.set_loading(False)
|
||||
image.set_from_file(f.result())
|
||||
|
||||
@@ -378,10 +381,10 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
return row
|
||||
|
||||
def update_search_results(self, search_results):
|
||||
def _update_search_results(self, search_results: SearchResult):
|
||||
# Songs
|
||||
if search_results.song is not None:
|
||||
self.remove_all_from_widget(self.song_results)
|
||||
self._remove_all_from_widget(self.song_results)
|
||||
for song in search_results.song or []:
|
||||
label_text = util.dot_join(
|
||||
f'<b>{util.esc(song.title)}</b>',
|
||||
@@ -390,54 +393,53 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
cover_art_future = CacheManager.get_cover_art_filename(
|
||||
song.coverArt, size=50)
|
||||
self.song_results.add(
|
||||
self.create_search_result_row(
|
||||
self._create_search_result_row(
|
||||
label_text, 'song', song, cover_art_future))
|
||||
|
||||
self.song_results.show_all()
|
||||
|
||||
# Albums
|
||||
if search_results.album is not None:
|
||||
self.remove_all_from_widget(self.album_results)
|
||||
self._remove_all_from_widget(self.album_results)
|
||||
for album in search_results.album or []:
|
||||
name = album.title if type(album) == Child else album.name
|
||||
label_text = util.dot_join(
|
||||
f'<b>{util.esc(name)}</b>',
|
||||
f'<b>{util.esc(album.name)}</b>',
|
||||
util.esc(album.artist),
|
||||
)
|
||||
cover_art_future = CacheManager.get_cover_art_filename(
|
||||
album.coverArt, size=50)
|
||||
self.album_results.add(
|
||||
self.create_search_result_row(
|
||||
self._create_search_result_row(
|
||||
label_text, 'album', album, cover_art_future))
|
||||
|
||||
self.album_results.show_all()
|
||||
|
||||
# Artists
|
||||
if search_results.artist is not None:
|
||||
self.remove_all_from_widget(self.artist_results)
|
||||
self._remove_all_from_widget(self.artist_results)
|
||||
for artist in search_results.artist or []:
|
||||
label_text = util.esc(artist.name)
|
||||
cover_art_future = CacheManager.get_artist_artwork(artist)
|
||||
self.artist_results.add(
|
||||
self.create_search_result_row(
|
||||
self._create_search_result_row(
|
||||
label_text, 'artist', artist, cover_art_future))
|
||||
|
||||
self.artist_results.show_all()
|
||||
|
||||
# Playlists
|
||||
if search_results.playlist is not None:
|
||||
self.remove_all_from_widget(self.playlist_results)
|
||||
self._remove_all_from_widget(self.playlist_results)
|
||||
for playlist in search_results.playlist or []:
|
||||
label_text = util.esc(playlist.name)
|
||||
cover_art_future = CacheManager.get_cover_art_filename(
|
||||
playlist.coverArt)
|
||||
self.playlist_results.add(
|
||||
self.create_search_result_row(
|
||||
self._create_search_result_row(
|
||||
label_text, 'playlist', playlist, cover_art_future))
|
||||
|
||||
self.playlist_results.show_all()
|
||||
|
||||
def event_in_widgets(self, event, *widgets):
|
||||
def _event_in_widgets(self, event: Gdk.EventButton, *widgets) -> bool:
|
||||
for widget in widgets:
|
||||
if not widget.is_visible():
|
||||
continue
|
||||
|
@@ -2,17 +2,18 @@ import math
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GdkPixbuf, Pango, GObject, GLib
|
||||
from gi.repository import GdkPixbuf, GLib, GObject, Gtk, Pango
|
||||
from pychromecast import Chromecast
|
||||
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.players import ChromecastPlayer
|
||||
from sublime.state_manager import ApplicationState, RepeatType
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import IconButton, SpinnerImage
|
||||
from sublime.players import ChromecastPlayer
|
||||
|
||||
|
||||
class PlayerControls(Gtk.ActionBar):
|
||||
@@ -169,7 +170,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
|
||||
new_store = []
|
||||
|
||||
def calculate_label(song_details):
|
||||
def calculate_label(song_details) -> str:
|
||||
title = util.esc(song_details.title)
|
||||
album = util.esc(song_details.album)
|
||||
artist = util.esc(song_details.artist)
|
||||
@@ -270,9 +271,9 @@ class PlayerControls(Gtk.ActionBar):
|
||||
def update_cover_art(
|
||||
self,
|
||||
cover_art_filename: str,
|
||||
state,
|
||||
force=False,
|
||||
order_token=None,
|
||||
state: ApplicationState,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
):
|
||||
if order_token != self.cover_art_update_order_token:
|
||||
return
|
||||
@@ -320,10 +321,10 @@ class PlayerControls(Gtk.ActionBar):
|
||||
{'no_reshuffle': True},
|
||||
)
|
||||
|
||||
def update_device_list(self, force=False):
|
||||
def update_device_list(self, force: bool = False):
|
||||
self.device_list_loading.show()
|
||||
|
||||
def chromecast_callback(chromecasts):
|
||||
def chromecast_callback(chromecasts: List[Chromecast]):
|
||||
self.chromecasts = chromecasts
|
||||
for c in self.chromecast_device_list.get_children():
|
||||
self.chromecast_device_list.remove(c)
|
||||
|
@@ -1,16 +1,15 @@
|
||||
from functools import lru_cache
|
||||
from random import randint
|
||||
from typing import List
|
||||
|
||||
from fuzzywuzzy import process
|
||||
from typing import Any, Iterable, List, Optional, Tuple
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gio, Gtk, Pango, GObject, GLib
|
||||
from fuzzywuzzy import process
|
||||
from gi.repository import Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.server.api_objects import PlaylistWithSongs
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import EditFormDialog, IconButton, SpinnerImage
|
||||
|
||||
@@ -21,7 +20,7 @@ class EditPlaylistDialog(EditFormDialog):
|
||||
text_fields = [('Name', 'name', False), ('Comment', 'comment', False)]
|
||||
boolean_fields = [('Public', 'public')]
|
||||
|
||||
def __init__(self, *args, playlist_id=None, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
delete_playlist = Gtk.Button(label='Delete Playlist')
|
||||
self.extra_buttons = [(delete_playlist, Gtk.ResponseType.NO)]
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -59,7 +58,7 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
)
|
||||
self.pack2(self.playlist_detail_panel, True, False)
|
||||
|
||||
def update(self, state: ApplicationState = None, force=False):
|
||||
def update(self, state: ApplicationState = None, force: bool = False):
|
||||
self.playlist_list.update(state=state, force=force)
|
||||
self.playlist_detail_panel.update(state=state, force=force)
|
||||
|
||||
@@ -77,7 +76,7 @@ class PlaylistList(Gtk.Box):
|
||||
playlist_id = GObject.Property(type=str)
|
||||
name = GObject.Property(type=str)
|
||||
|
||||
def __init__(self, playlist_id, name):
|
||||
def __init__(self, playlist_id: str, name: str):
|
||||
GObject.GObject.__init__(self)
|
||||
self.playlist_id = playlist_id
|
||||
self.name = name
|
||||
@@ -144,7 +143,8 @@ class PlaylistList(Gtk.Box):
|
||||
|
||||
list_scroll_window = Gtk.ScrolledWindow(min_content_width=220)
|
||||
|
||||
def create_playlist_row(model: PlaylistList.PlaylistModel):
|
||||
def create_playlist_row(
|
||||
model: PlaylistList.PlaylistModel) -> Gtk.ListBoxRow:
|
||||
row = Gtk.ListBoxRow(
|
||||
action_name='app.go-to-playlist',
|
||||
action_target=GLib.Variant('s', model.playlist_id),
|
||||
@@ -180,8 +180,8 @@ class PlaylistList(Gtk.Box):
|
||||
self,
|
||||
playlists: List[PlaylistWithSongs],
|
||||
state: ApplicationState,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
):
|
||||
new_store = []
|
||||
selected_idx = None
|
||||
@@ -203,25 +203,25 @@ class PlaylistList(Gtk.Box):
|
||||
|
||||
# Event Handlers
|
||||
# =========================================================================
|
||||
def on_new_playlist_clicked(self, new_playlist_button):
|
||||
def on_new_playlist_clicked(self, _: Any):
|
||||
self.new_playlist_entry.set_text('Untitled Playlist')
|
||||
self.new_playlist_entry.grab_focus()
|
||||
self.new_playlist_row.show()
|
||||
|
||||
def on_list_refresh_click(self, button):
|
||||
def on_list_refresh_click(self, _: Any):
|
||||
self.update(force=True)
|
||||
|
||||
def new_entry_activate(self, entry):
|
||||
def new_entry_activate(self, entry: Gtk.Entry):
|
||||
self.create_playlist(entry.get_text())
|
||||
|
||||
def cancel_button_clicked(self, button):
|
||||
def cancel_button_clicked(self, _: Any):
|
||||
self.new_playlist_row.hide()
|
||||
|
||||
def confirm_button_clicked(self, button):
|
||||
def confirm_button_clicked(self, _: Any):
|
||||
self.create_playlist(self.new_playlist_entry.get_text())
|
||||
|
||||
def create_playlist(self, playlist_name):
|
||||
def on_playlist_created(f):
|
||||
def create_playlist(self, playlist_name: str):
|
||||
def on_playlist_created(_: Any):
|
||||
CacheManager.invalidate_playlists_cache()
|
||||
self.update(force=True)
|
||||
|
||||
@@ -339,7 +339,13 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
# Playlist songs list
|
||||
playlist_view_scroll_window = Gtk.ScrolledWindow()
|
||||
|
||||
def create_column(header, text_idx, bold=False, align=0, width=None):
|
||||
def create_column(
|
||||
header: str,
|
||||
text_idx: int,
|
||||
bold: bool = False,
|
||||
align: int = 0,
|
||||
width: int = None,
|
||||
):
|
||||
renderer = Gtk.CellRendererText(
|
||||
xalign=align,
|
||||
weight=Pango.Weight.BOLD if bold else Pango.Weight.NORMAL,
|
||||
@@ -362,11 +368,11 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
)
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def row_score(key, row_items):
|
||||
def row_score(key: str, row_items: Iterable[str]) -> int:
|
||||
return max(map(lambda m: m[1], process.extract(key, row_items)))
|
||||
|
||||
@lru_cache()
|
||||
def max_score_for_key(key, rows):
|
||||
def max_score_for_key(key: str, rows: Tuple) -> int:
|
||||
return max(row_score(key, row) for row in rows)
|
||||
|
||||
def playlist_song_list_search_fn(model, col, key, treeiter, data=None):
|
||||
@@ -433,7 +439,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
update_playlist_view_order_token = 0
|
||||
|
||||
def update(self, state: ApplicationState, force=False):
|
||||
def update(self, state: ApplicationState, force: bool = False):
|
||||
if state.selected_playlist_id is None:
|
||||
self.playlist_artwork.set_from_file(None)
|
||||
self.playlist_indicator.set_markup('')
|
||||
@@ -460,10 +466,10 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
)
|
||||
def update_playlist_view(
|
||||
self,
|
||||
playlist,
|
||||
playlist: PlaylistWithSongs,
|
||||
state: ApplicationState = None,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
):
|
||||
if self.update_playlist_view_order_token != order_token:
|
||||
return
|
||||
@@ -483,7 +489,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
self.playlist_comment.show()
|
||||
else:
|
||||
self.playlist_comment.hide()
|
||||
self.playlist_stats.set_markup(self.format_stats(playlist))
|
||||
self.playlist_stats.set_markup(self._format_stats(playlist))
|
||||
|
||||
# Update the artwork.
|
||||
self.update_playlist_artwork(
|
||||
@@ -522,10 +528,10 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
)
|
||||
def update_playlist_artwork(
|
||||
self,
|
||||
cover_art_filename,
|
||||
cover_art_filename: str,
|
||||
state: ApplicationState,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
):
|
||||
if self.update_playlist_view_order_token != order_token:
|
||||
return
|
||||
@@ -535,18 +541,17 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
# Event Handlers
|
||||
# =========================================================================
|
||||
def on_view_refresh_click(self, button):
|
||||
def on_view_refresh_click(self, _: Any):
|
||||
self.update_playlist_view(
|
||||
self.playlist_id,
|
||||
force=True,
|
||||
order_token=self.update_playlist_view_order_token,
|
||||
)
|
||||
|
||||
def on_playlist_edit_button_click(self, button):
|
||||
def on_playlist_edit_button_click(self, _: Any):
|
||||
dialog = EditPlaylistDialog(
|
||||
self.get_toplevel(),
|
||||
CacheManager.get_playlist(self.playlist_id).result(),
|
||||
playlist_id=self.playlist_id,
|
||||
)
|
||||
|
||||
result = dialog.run()
|
||||
@@ -577,7 +582,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def on_playlist_list_download_all_button_click(self, button):
|
||||
def on_playlist_list_download_all_button_click(self, _: Any):
|
||||
def download_state_change(*args):
|
||||
GLib.idle_add(
|
||||
lambda: self.update_playlist_view(
|
||||
@@ -592,7 +597,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
on_song_download_complete=download_state_change,
|
||||
)
|
||||
|
||||
def on_play_all_clicked(self, btn):
|
||||
def on_play_all_clicked(self, _: Any):
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
0,
|
||||
@@ -603,7 +608,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
},
|
||||
)
|
||||
|
||||
def on_shuffle_all_button(self, btn):
|
||||
def on_shuffle_all_button(self, _: Any):
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
randint(0,
|
||||
@@ -635,7 +640,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
store, paths = tree.get_selection().get_selected_rows()
|
||||
allow_deselect = False
|
||||
|
||||
def on_download_state_change(song_id=None):
|
||||
def on_download_state_change(**kwargs):
|
||||
GLib.idle_add(
|
||||
lambda: self.update_playlist_view(
|
||||
self.playlist_id,
|
||||
@@ -656,7 +661,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
widget_coords = tree.convert_tree_to_widget_coords(
|
||||
event.x, event.y)
|
||||
|
||||
def on_remove_songs_click(button):
|
||||
def on_remove_songs_click(_: Any):
|
||||
CacheManager.update_playlist(
|
||||
playlist_id=self.playlist_id,
|
||||
song_index_to_remove=[p.get_indices()[0] for p in paths],
|
||||
@@ -694,7 +699,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
# which one comes first, but just in case, we have this
|
||||
# reordering_playlist_song_list flag.
|
||||
if self.reordering_playlist_song_list:
|
||||
self.update_playlist_order(self.playlist_id)
|
||||
self._update_playlist_order(self.playlist_id)
|
||||
self.reordering_playlist_song_list = False
|
||||
else:
|
||||
self.reordering_playlist_song_list = True
|
||||
@@ -705,7 +710,12 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
self.playlist_artwork.set_loading(True)
|
||||
self.playlist_view_loading_box.show_all()
|
||||
|
||||
def make_label(self, text=None, name=None, **params):
|
||||
def make_label(
|
||||
self,
|
||||
text: str = None,
|
||||
name: str = None,
|
||||
**params,
|
||||
) -> Gtk.Label:
|
||||
return Gtk.Label(
|
||||
label=text,
|
||||
name=name,
|
||||
@@ -714,7 +724,12 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
)
|
||||
|
||||
@util.async_callback(lambda *a, **k: CacheManager.get_playlist(*a, **k))
|
||||
def update_playlist_order(self, playlist, state, **kwargs):
|
||||
def _update_playlist_order(
|
||||
self,
|
||||
playlist: PlaylistWithSongs,
|
||||
state: ApplicationState,
|
||||
**kwargs,
|
||||
):
|
||||
self.playlist_view_loading_box.show_all()
|
||||
update_playlist_future = CacheManager.update_playlist(
|
||||
playlist_id=playlist.id,
|
||||
@@ -730,7 +745,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
order_token=self.update_playlist_view_order_token,
|
||||
)))
|
||||
|
||||
def format_stats(self, playlist):
|
||||
def _format_stats(self, playlist: PlaylistWithSongs) -> str:
|
||||
created_date = playlist.created.strftime('%B %d, %Y')
|
||||
lines = [
|
||||
util.dot_join(
|
||||
|
@@ -1,19 +1,18 @@
|
||||
import functools
|
||||
from typing import Callable, List, Tuple, Any
|
||||
import re
|
||||
|
||||
from concurrent.futures import Future
|
||||
|
||||
from deepdiff import DeepDiff
|
||||
from typing import Any, Callable, cast, List, Match, Optional, Tuple, Union
|
||||
|
||||
import gi
|
||||
from deepdiff import DeepDiff
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib, Gdk
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from sublime.cache_manager import CacheManager, SongCacheStatus
|
||||
from sublime.state_manager import ApplicationState
|
||||
|
||||
|
||||
def format_song_duration(duration_secs) -> str:
|
||||
def format_song_duration(duration_secs: int) -> str:
|
||||
"""
|
||||
Formats the song duration as mins:seconds with the seconds being
|
||||
zero-padded if necessary.
|
||||
@@ -26,7 +25,11 @@ def format_song_duration(duration_secs) -> str:
|
||||
return f'{duration_secs // 60}:{duration_secs % 60:02}'
|
||||
|
||||
|
||||
def pluralize(string: str, number: int, pluralized_form=None):
|
||||
def pluralize(
|
||||
string: str,
|
||||
number: int,
|
||||
pluralized_form: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Pluralize the given string given the count as a number.
|
||||
|
||||
@@ -42,7 +45,7 @@ def pluralize(string: str, number: int, pluralized_form=None):
|
||||
return string
|
||||
|
||||
|
||||
def format_sequence_duration(duration_secs) -> str:
|
||||
def format_sequence_duration(duration_secs: int) -> str:
|
||||
"""
|
||||
Formats duration in English.
|
||||
|
||||
@@ -76,20 +79,20 @@ def format_sequence_duration(duration_secs) -> str:
|
||||
return ', '.join(format_components)
|
||||
|
||||
|
||||
def esc(string):
|
||||
def esc(string: str) -> str:
|
||||
if string is None:
|
||||
return None
|
||||
return string.replace('&', '&').replace(" target='_blank'", '')
|
||||
|
||||
|
||||
def dot_join(*items):
|
||||
def dot_join(*items: Any) -> str:
|
||||
"""
|
||||
Joins the given strings with a dot character. Filters out None values.
|
||||
"""
|
||||
return ' • '.join(map(str, filter(lambda x: x is not None, items)))
|
||||
|
||||
|
||||
def get_cached_status_icon(cache_status: SongCacheStatus):
|
||||
def get_cached_status_icon(cache_status: SongCacheStatus) -> str:
|
||||
cache_icon = {
|
||||
SongCacheStatus.NOT_CACHED: '',
|
||||
SongCacheStatus.CACHED: 'folder-download-symbolic',
|
||||
@@ -99,9 +102,9 @@ def get_cached_status_icon(cache_status: SongCacheStatus):
|
||||
return cache_icon[cache_status]
|
||||
|
||||
|
||||
def _parse_diff_location(location):
|
||||
def _parse_diff_location(location: str):
|
||||
match = re.match(r'root\[(\d*)\](?:\[(\d*)\]|\.(.*))?', location)
|
||||
return tuple(g for g in match.groups() if g is not None)
|
||||
return tuple(g for g in cast(Match, match).groups() if g is not None)
|
||||
|
||||
|
||||
def diff_song_store(store_to_edit, new_store):
|
||||
@@ -302,9 +305,9 @@ def show_song_popover(
|
||||
|
||||
|
||||
def async_callback(
|
||||
future_fn,
|
||||
before_download=None,
|
||||
on_failure=None,
|
||||
future_fn: Callable[..., Future],
|
||||
before_download: Callable[[Any], None] = None,
|
||||
on_failure: Callable[[Any, Exception], None] = None,
|
||||
):
|
||||
"""
|
||||
Defines the ``async_callback`` decorator.
|
||||
@@ -315,16 +318,16 @@ def async_callback(
|
||||
by said lambda function.
|
||||
|
||||
:param future_fn: a function which generates a
|
||||
``concurrent.futures.Future``.
|
||||
:class:`concurrent.futures.Future` or :class:`CacheManager.Result`.
|
||||
"""
|
||||
def decorator(callback_fn):
|
||||
@functools.wraps(callback_fn)
|
||||
def wrapper(
|
||||
self,
|
||||
*args,
|
||||
state=None,
|
||||
order_token=None,
|
||||
force=False,
|
||||
state: ApplicationState = None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if before_download:
|
||||
@@ -333,15 +336,15 @@ def async_callback(
|
||||
else:
|
||||
on_before_download = (lambda: None)
|
||||
|
||||
def future_callback(f):
|
||||
def future_callback(f: Union[Future, CacheManager.Result]):
|
||||
try:
|
||||
result = f.result()
|
||||
except Exception as e:
|
||||
if on_failure:
|
||||
on_failure(self, e)
|
||||
GLib.idle_add(on_failure, self, e)
|
||||
return
|
||||
|
||||
return GLib.idle_add(
|
||||
GLib.idle_add(
|
||||
lambda: callback_fn(
|
||||
self,
|
||||
result,
|
||||
@@ -350,7 +353,7 @@ def async_callback(
|
||||
order_token=order_token,
|
||||
))
|
||||
|
||||
future: Future = future_fn(
|
||||
future: Union[Future, CacheManager.Result] = future_fn(
|
||||
*args,
|
||||
before_download=on_before_download,
|
||||
force=force,
|
||||
|
Reference in New Issue
Block a user