Python3 Migrate
This commit is contained in:
31
venv/lib/python3.7/site-packages/mopidy/m3u/__init__.py
Normal file
31
venv/lib/python3.7/site-packages/mopidy/m3u/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import mopidy
|
||||
from mopidy import config, ext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Extension(ext.Extension):
|
||||
|
||||
dist_name = "Mopidy-M3U"
|
||||
ext_name = "m3u"
|
||||
version = mopidy.__version__
|
||||
|
||||
def get_default_config(self):
|
||||
conf_file = os.path.join(os.path.dirname(__file__), "ext.conf")
|
||||
return config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
schema = super().get_config_schema()
|
||||
schema["base_dir"] = config.Path(optional=True)
|
||||
schema["default_encoding"] = config.String()
|
||||
schema["default_extension"] = config.String(choices=[".m3u", ".m3u8"])
|
||||
schema["playlists_dir"] = config.Path(optional=True)
|
||||
return schema
|
||||
|
||||
def setup(self, registry):
|
||||
from .backend import M3UBackend
|
||||
|
||||
registry.add("backend", M3UBackend)
|
||||
13
venv/lib/python3.7/site-packages/mopidy/m3u/backend.py
Normal file
13
venv/lib/python3.7/site-packages/mopidy/m3u/backend.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import pykka
|
||||
|
||||
from mopidy import backend
|
||||
|
||||
from . import playlists
|
||||
|
||||
|
||||
class M3UBackend(pykka.ThreadingActor, backend.Backend):
|
||||
uri_schemes = ["m3u"]
|
||||
|
||||
def __init__(self, config, audio):
|
||||
super().__init__()
|
||||
self.playlists = playlists.M3UPlaylistsProvider(self, config)
|
||||
6
venv/lib/python3.7/site-packages/mopidy/m3u/ext.conf
Normal file
6
venv/lib/python3.7/site-packages/mopidy/m3u/ext.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
[m3u]
|
||||
enabled = true
|
||||
playlists_dir =
|
||||
base_dir = $XDG_MUSIC_DIR
|
||||
default_encoding = latin-1
|
||||
default_extension = .m3u8
|
||||
174
venv/lib/python3.7/site-packages/mopidy/m3u/playlists.py
Normal file
174
venv/lib/python3.7/site-packages/mopidy/m3u/playlists.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import contextlib
|
||||
import locale
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
from mopidy import backend
|
||||
from mopidy.internal import path
|
||||
|
||||
from . import Extension, translator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def log_environment_error(message, error):
|
||||
if isinstance(error.strerror, bytes):
|
||||
strerror = error.strerror.decode(locale.getpreferredencoding())
|
||||
else:
|
||||
strerror = error.strerror
|
||||
logger.error("%s: %s", message, strerror)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def replace(path, mode="w+b", encoding=None, errors=None):
|
||||
(fd, tempname) = tempfile.mkstemp(dir=str(path.parent))
|
||||
tempname = pathlib.Path(tempname)
|
||||
try:
|
||||
fp = open(fd, mode, encoding=encoding, errors=errors)
|
||||
except Exception:
|
||||
tempname.unlink()
|
||||
os.close(fd)
|
||||
raise
|
||||
try:
|
||||
yield fp
|
||||
fp.flush()
|
||||
os.fsync(fd)
|
||||
tempname.rename(path)
|
||||
except Exception:
|
||||
tempname.unlink()
|
||||
raise
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
class M3UPlaylistsProvider(backend.PlaylistsProvider):
|
||||
def __init__(self, backend, config):
|
||||
super().__init__(backend)
|
||||
|
||||
ext_config = config[Extension.ext_name]
|
||||
if ext_config["playlists_dir"] is None:
|
||||
self._playlists_dir = Extension.get_data_dir(config)
|
||||
else:
|
||||
self._playlists_dir = path.expand_path(ext_config["playlists_dir"])
|
||||
if ext_config["base_dir"] is None:
|
||||
self._base_dir = self._playlists_dir
|
||||
else:
|
||||
self._base_dir = path.expand_path(ext_config["base_dir"])
|
||||
self._default_encoding = ext_config["default_encoding"]
|
||||
self._default_extension = ext_config["default_extension"]
|
||||
|
||||
def as_list(self):
|
||||
result = []
|
||||
for entry in self._playlists_dir.iterdir():
|
||||
if entry.suffix not in [".m3u", ".m3u8"]:
|
||||
continue
|
||||
elif not entry.is_file():
|
||||
continue
|
||||
else:
|
||||
playlist_path = entry.relative_to(self._playlists_dir)
|
||||
result.append(translator.path_to_ref(playlist_path))
|
||||
result.sort(key=operator.attrgetter("name"))
|
||||
return result
|
||||
|
||||
def create(self, name):
|
||||
path = translator.path_from_name(name.strip(), self._default_extension)
|
||||
try:
|
||||
with self._open(path, "w"):
|
||||
pass
|
||||
mtime = self._abspath(path).stat().st_mtime
|
||||
except OSError as e:
|
||||
log_environment_error(f"Error creating playlist {name!r}", e)
|
||||
else:
|
||||
return translator.playlist(path, [], mtime)
|
||||
|
||||
def delete(self, uri):
|
||||
path = translator.uri_to_path(uri)
|
||||
if not self._is_in_basedir(path):
|
||||
logger.debug("Ignoring path outside playlist dir: %s", uri)
|
||||
return False
|
||||
try:
|
||||
self._abspath(path).unlink()
|
||||
except OSError as e:
|
||||
log_environment_error(f"Error deleting playlist {uri!r}", e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def get_items(self, uri):
|
||||
path = translator.uri_to_path(uri)
|
||||
if not self._is_in_basedir(path):
|
||||
logger.debug("Ignoring path outside playlist dir: %s", uri)
|
||||
return None
|
||||
try:
|
||||
with self._open(path, "r") as fp:
|
||||
items = translator.load_items(fp, self._base_dir)
|
||||
except OSError as e:
|
||||
log_environment_error(f"Error reading playlist {uri!r}", e)
|
||||
else:
|
||||
return items
|
||||
|
||||
def lookup(self, uri):
|
||||
path = translator.uri_to_path(uri)
|
||||
if not self._is_in_basedir(path):
|
||||
logger.debug("Ignoring path outside playlist dir: %s", uri)
|
||||
return None
|
||||
try:
|
||||
with self._open(path, "r") as fp:
|
||||
items = translator.load_items(fp, self._base_dir)
|
||||
mtime = self._abspath(path).stat().st_mtime
|
||||
except OSError as e:
|
||||
log_environment_error(f"Error reading playlist {uri!r}", e)
|
||||
else:
|
||||
return translator.playlist(path, items, mtime)
|
||||
|
||||
def refresh(self):
|
||||
pass # nothing to do
|
||||
|
||||
def save(self, playlist):
|
||||
path = translator.uri_to_path(playlist.uri)
|
||||
if not self._is_in_basedir(path):
|
||||
logger.debug("Ignoring path outside playlist dir: %s", playlist.uri)
|
||||
return None
|
||||
name = translator.name_from_path(path)
|
||||
try:
|
||||
with self._open(path, "w") as fp:
|
||||
translator.dump_items(playlist.tracks, fp)
|
||||
if playlist.name and playlist.name != name:
|
||||
orig_path = path
|
||||
path = translator.path_from_name(playlist.name.strip())
|
||||
path = path.with_suffix(orig_path.suffix)
|
||||
self._abspath(orig_path).rename(self._abspath(path))
|
||||
mtime = self._abspath(path).stat().st_mtime
|
||||
except OSError as e:
|
||||
log_environment_error(f"Error saving playlist {playlist.uri!r}", e)
|
||||
else:
|
||||
return translator.playlist(path, playlist.tracks, mtime)
|
||||
|
||||
def _abspath(self, path):
|
||||
if not path.is_absolute():
|
||||
return self._playlists_dir / path
|
||||
else:
|
||||
return path
|
||||
|
||||
def _is_in_basedir(self, local_path):
|
||||
local_path = self._abspath(local_path)
|
||||
return path.is_path_inside_base_dir(local_path, self._playlists_dir)
|
||||
|
||||
def _open(self, path, mode="r"):
|
||||
if path.suffix == ".m3u8":
|
||||
encoding = "utf-8"
|
||||
else:
|
||||
encoding = self._default_encoding
|
||||
if not path.is_absolute():
|
||||
path = self._abspath(path)
|
||||
if not self._is_in_basedir(path):
|
||||
raise Exception(
|
||||
f"Path {path!r} is not inside playlist dir {self._playlist_dir!r}"
|
||||
)
|
||||
if "w" in mode:
|
||||
return replace(path, mode, encoding=encoding, errors="replace")
|
||||
else:
|
||||
return path.open(mode, encoding=encoding, errors="replace")
|
||||
87
venv/lib/python3.7/site-packages/mopidy/m3u/translator.py
Normal file
87
venv/lib/python3.7/site-packages/mopidy/m3u/translator.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
import pathlib
|
||||
import urllib
|
||||
|
||||
from mopidy import models
|
||||
from mopidy.internal import path
|
||||
|
||||
from . import Extension
|
||||
|
||||
|
||||
def path_to_uri(path, scheme=Extension.ext_name):
|
||||
"""Convert file path to URI."""
|
||||
bytes_path = os.path.normpath(bytes(path))
|
||||
uripath = urllib.parse.quote_from_bytes(bytes_path)
|
||||
return urllib.parse.urlunsplit((scheme, None, uripath, None, None))
|
||||
|
||||
|
||||
def uri_to_path(uri):
|
||||
"""Convert URI to file path."""
|
||||
return path.uri_to_path(uri)
|
||||
|
||||
|
||||
def name_from_path(path):
|
||||
"""Extract name from file path."""
|
||||
name = bytes(pathlib.Path(path.stem))
|
||||
try:
|
||||
return name.decode(errors="replace")
|
||||
except UnicodeError:
|
||||
return None
|
||||
|
||||
|
||||
def path_from_name(name, ext=None, sep="|"):
|
||||
"""Convert name with optional extension to file path."""
|
||||
if ext:
|
||||
name = name.replace(os.sep, sep) + ext
|
||||
else:
|
||||
name = name.replace(os.sep, sep)
|
||||
return pathlib.Path(name)
|
||||
|
||||
|
||||
def path_to_ref(path):
|
||||
return models.Ref.playlist(uri=path_to_uri(path), name=name_from_path(path))
|
||||
|
||||
|
||||
def load_items(fp, basedir):
|
||||
refs = []
|
||||
name = None
|
||||
for line in filter(None, (line.strip() for line in fp)):
|
||||
if line.startswith("#"):
|
||||
if line.startswith("#EXTINF:"):
|
||||
name = line.partition(",")[2]
|
||||
continue
|
||||
elif not urllib.parse.urlsplit(line).scheme:
|
||||
path = basedir / line
|
||||
if not name:
|
||||
name = name_from_path(path)
|
||||
uri = path_to_uri(path, scheme="file")
|
||||
else:
|
||||
# TODO: ensure this is urlencoded
|
||||
uri = line # do *not* extract name from (stream?) URI path
|
||||
refs.append(models.Ref.track(uri=uri, name=name))
|
||||
name = None
|
||||
return refs
|
||||
|
||||
|
||||
def dump_items(items, fp):
|
||||
if any(item.name for item in items):
|
||||
print("#EXTM3U", file=fp)
|
||||
for item in items:
|
||||
if item.name:
|
||||
print(f"#EXTINF:-1,{item.name}", file=fp)
|
||||
# TODO: convert file URIs to (relative) paths?
|
||||
if isinstance(item.uri, bytes):
|
||||
print(item.uri.decode(), file=fp)
|
||||
else:
|
||||
print(item.uri, file=fp)
|
||||
|
||||
|
||||
def playlist(path, items=None, mtime=None):
|
||||
if items is None:
|
||||
items = []
|
||||
return models.Playlist(
|
||||
uri=path_to_uri(path),
|
||||
name=name_from_path(path),
|
||||
tracks=[models.Track(uri=item.uri, name=item.name) for item in items],
|
||||
last_modified=(int(mtime * 1000) if mtime else None),
|
||||
)
|
||||
Reference in New Issue
Block a user