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