Merge branch 'develop' into feature/mpris-frontend
Conflicts: docs/changes.rst
This commit is contained in:
commit
fae469cca3
@ -1,31 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.scanner import Scanner, translator
|
||||
from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format
|
||||
from mopidy import settings
|
||||
from mopidy.utils.log import setup_console_logging, setup_root_logger
|
||||
from mopidy.scanner import Scanner, translator
|
||||
from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format
|
||||
|
||||
tracks = []
|
||||
setup_root_logger()
|
||||
setup_console_logging(2)
|
||||
|
||||
def store(data):
|
||||
track = translator(data)
|
||||
tracks.append(track)
|
||||
print >> sys.stderr, 'Added %s' % track.uri
|
||||
tracks = []
|
||||
|
||||
def debug(uri, error):
|
||||
print >> sys.stderr, 'Failed %s: %s' % (uri, error)
|
||||
def store(data):
|
||||
track = translator(data)
|
||||
tracks.append(track)
|
||||
logging.debug(u'Added %s', track.uri)
|
||||
|
||||
print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH
|
||||
def debug(uri, error, debug):
|
||||
logging.error(u'Failed %s: %s - %s', uri, error, debug)
|
||||
|
||||
scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
|
||||
logging.info(u'Scanning %s', settings.LOCAL_MUSIC_PATH)
|
||||
|
||||
scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
|
||||
try:
|
||||
scanner.start()
|
||||
except KeyboardInterrupt:
|
||||
scanner.stop()
|
||||
|
||||
print >> sys.stderr, 'Done'
|
||||
logging.info(u'Done')
|
||||
|
||||
for a in tracks_to_tag_cache_format(tracks):
|
||||
if len(a) == 1:
|
||||
print (u'%s' % a).encode('utf-8')
|
||||
else:
|
||||
print (u'%s: %s' % a).encode('utf-8')
|
||||
for a in tracks_to_tag_cache_format(tracks):
|
||||
if len(a) == 1:
|
||||
print (u'%s' % a).encode('utf-8')
|
||||
else:
|
||||
print (u'%s: %s' % a).encode('utf-8')
|
||||
|
||||
BIN
docs/_static/thread_communication.png
vendored
BIN
docs/_static/thread_communication.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
@ -5,7 +5,7 @@ Authors
|
||||
Contributors to Mopidy in the order of appearance:
|
||||
|
||||
- Stein Magnus Jodal <stein.magnus@jodal.no>
|
||||
- Johannes Knutsen <johannes@knutseninfo.no>
|
||||
- Johannes Knutsen <johannes@knutsen.me>
|
||||
- Thomas Adamcik <adamcik@samfundet.no>
|
||||
- Kristian Klette <klette@klette.us>
|
||||
|
||||
|
||||
@ -8,7 +8,9 @@ This change log is used to track all major changes to Mopidy.
|
||||
v0.5.0 (in development)
|
||||
=======================
|
||||
|
||||
No description yet.
|
||||
Since last time we've added support for audio streaming to SHOUTcast servers
|
||||
and fixed the longstanding playlist loading issue in the Spotify backend. As
|
||||
always the release has a bunch of bug fixes.
|
||||
|
||||
Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
*Important changes* below.
|
||||
@ -20,22 +22,21 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
automatically be upgraded. If you are not installing from APT, follow the
|
||||
instructions at :doc:`/installation/libspotify/`.
|
||||
|
||||
- Mopidy now supports running with 1-n outputs at the same time. This feature
|
||||
was mainly added to facilitate Shoutcast support, which Mopidy has also
|
||||
gained. In its current state outputs can not be toggled during runtime.
|
||||
- If you have explicitly set the :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE`
|
||||
setting, you must update your settings file. The new setting is named
|
||||
:attr:`mopidy.settings.SPOTIFY_BITRATE` and accepts the integer values 96,
|
||||
160, and 320.
|
||||
|
||||
- Mopidy now supports running with 1 to N outputs at the same time. This
|
||||
feature was mainly added to facilitate SHOUTcast support, which Mopidy has
|
||||
also gained. In its current state outputs can not be toggled during runtime.
|
||||
|
||||
**Changes**
|
||||
|
||||
- Fix local backend time query errors that where coming from stopped pipeline.
|
||||
(Fixes: :issue:`87`)
|
||||
- Local backend:
|
||||
|
||||
- Support passing options to GStreamer. See :option:`--help-gst` for a list of
|
||||
available options. (Fixes: :issue:`95`)
|
||||
|
||||
- Improve :option:`--list-settings` output. (Fixes: :issue:`91`)
|
||||
|
||||
- Replace not decodable characters returned from Spotify instead of throwing an
|
||||
exception, as we won't try to figure out the encoding of non-UTF-8-data.
|
||||
- Fix local backend time query errors that where coming from stopped
|
||||
pipeline. (Fixes: :issue:`87`)
|
||||
|
||||
- Spotify backend:
|
||||
|
||||
@ -44,6 +45,9 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
workaround of searching and reconnecting to make the playlists appear are
|
||||
no longer necessary. (Fixes: :issue:`59`)
|
||||
|
||||
- Replace not decodable characters returned from Spotify instead of throwing
|
||||
an exception, as we won't try to figure out the encoding of non-UTF-8-data.
|
||||
|
||||
- MPD frontend:
|
||||
|
||||
- Refactoring and cleanup. Most notably, all request handlers now get an
|
||||
@ -59,7 +63,27 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
authentication is turned on, but the connected user has not been
|
||||
authenticated yet.
|
||||
|
||||
- Backends:
|
||||
- Command line usage:
|
||||
|
||||
- Support passing options to GStreamer. See :option:`--help-gst` for a list
|
||||
of available options. (Fixes: :issue:`95`)
|
||||
|
||||
- Improve :option:`--list-settings` output. (Fixes: :issue:`91`)
|
||||
|
||||
- Added :option:`--interactive` for reading missing local settings from
|
||||
``stdin``. (Fixes: :issue:`96`)
|
||||
|
||||
- Tag cache generator:
|
||||
|
||||
- Made it possible to abort :command:`mopidy-scan` with CTRL+C.
|
||||
|
||||
- Fixed bug regarding handling of bad dates.
|
||||
|
||||
- Use :mod:`logging` instead of ``print`` statements.
|
||||
|
||||
- Found and worked around strange WMA metadata behaviour.
|
||||
|
||||
- Backend API:
|
||||
|
||||
- Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next`
|
||||
and :meth:`mopidy.backends.base.playback.PlaybackController.previous` no
|
||||
|
||||
@ -7,4 +7,3 @@ Development
|
||||
|
||||
roadmap
|
||||
contributing
|
||||
internals
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
*********
|
||||
Internals
|
||||
*********
|
||||
|
||||
Some of the following notes and details will hopefully be useful when you start
|
||||
developing on Mopidy, while some may only be useful when you get deeper into
|
||||
specific parts of Mopidy.
|
||||
|
||||
In addition to what you'll find here, don't forget the :doc:`/api/index`.
|
||||
|
||||
|
||||
Class instantiation and usage
|
||||
=============================
|
||||
|
||||
The following diagram shows how Mopidy is wired together with the MPD client,
|
||||
the Spotify service, and the speakers.
|
||||
|
||||
**Legend**
|
||||
|
||||
- Filled red boxes are the key external systems.
|
||||
- Gray boxes are external dependencies.
|
||||
- Blue circles lives in the ``main`` process, also known as ``CoreProcess``.
|
||||
It is processing messages put on the core queue.
|
||||
- Purple circles lives in a process named ``MpdProcess``, running an
|
||||
:mod:`asyncore` loop.
|
||||
- Green circles lives in a process named ``GStreamerProcess``.
|
||||
- Brown circle is a thread living in the ``CoreProcess``.
|
||||
|
||||
.. digraph:: class_instantiation_and_usage
|
||||
|
||||
"main" [ color="blue" ]
|
||||
"CoreProcess" [ color="blue" ]
|
||||
|
||||
# Frontend
|
||||
"MPD client" [ color="red", style="filled", shape="box" ]
|
||||
"MpdFrontend" [ color="blue" ]
|
||||
"MpdProcess" [ color="purple" ]
|
||||
"MpdServer" [ color="purple" ]
|
||||
"MpdSession" [ color="purple" ]
|
||||
"MpdDispatcher" [ color="blue" ]
|
||||
|
||||
# Backend
|
||||
"Libspotify\nBackend" [ color="blue" ]
|
||||
"Libspotify\nSessionManager" [ color="brown" ]
|
||||
"pyspotify" [ color="gray", shape="box" ]
|
||||
"libspotify" [ color="gray", shape="box" ]
|
||||
"Spotify" [ color="red", style="filled", shape="box" ]
|
||||
|
||||
# Output/mixer
|
||||
"GStreamer\nOutput" [ color="blue" ]
|
||||
"GStreamer\nSoftwareMixer" [ color="blue" ]
|
||||
"GStreamer\nProcess" [ color="green" ]
|
||||
"GStreamer" [ color="gray", shape="box" ]
|
||||
"Speakers" [ color="red", style="filled", shape="box" ]
|
||||
|
||||
"main" -> "CoreProcess" [ label="create" ]
|
||||
|
||||
# Frontend
|
||||
"CoreProcess" -> "MpdFrontend" [ label="create" ]
|
||||
"MpdFrontend" -> "MpdProcess" [ label="create" ]
|
||||
"MpdFrontend" -> "MpdDispatcher" [ label="create" ]
|
||||
"MpdProcess" -> "MpdServer" [ label="create" ]
|
||||
"MpdServer" -> "MpdSession" [ label="create one\nper client" ]
|
||||
"MpdSession" -> "MpdDispatcher" [
|
||||
label="pass requests\nvia core_queue" ]
|
||||
"MpdDispatcher" -> "MpdSession" [
|
||||
label="pass response\nvia reply_to pipe" ]
|
||||
"MpdDispatcher" -> "Libspotify\nBackend" [ label="use backend API" ]
|
||||
"MPD client" -> "MpdServer" [ label="connect" ]
|
||||
"MPD client" -> "MpdSession" [ label="request" ]
|
||||
"MpdSession" -> "MPD client" [ label="response" ]
|
||||
|
||||
# Backend
|
||||
"CoreProcess" -> "Libspotify\nBackend" [ label="create" ]
|
||||
"Libspotify\nBackend" -> "Libspotify\nSessionManager" [
|
||||
label="creates and uses" ]
|
||||
"Libspotify\nSessionManager" -> "Libspotify\nBackend" [
|
||||
label="pass commands\nvia core_queue" ]
|
||||
"Libspotify\nSessionManager" -> "pyspotify" [ label="use Python\nwrapper" ]
|
||||
"pyspotify" -> "Libspotify\nSessionManager" [ label="use callbacks" ]
|
||||
"pyspotify" -> "libspotify" [ label="use C library" ]
|
||||
"libspotify" -> "Spotify" [ label="use service" ]
|
||||
"Libspotify\nSessionManager" -> "GStreamer\nProcess" [
|
||||
label="pass commands\nand audio data\nvia output_queue" ]
|
||||
|
||||
# Output/mixer
|
||||
"Libspotify\nBackend" -> "GStreamer\nSoftwareMixer" [
|
||||
label="create and\nuse mixer API" ]
|
||||
"GStreamer\nSoftwareMixer" -> "GStreamer\nProcess" [
|
||||
label="pass commands\nvia output_queue" ]
|
||||
"CoreProcess" -> "GStreamer\nOutput" [ label="create" ]
|
||||
"GStreamer\nOutput" -> "GStreamer\nProcess" [ label="create" ]
|
||||
"GStreamer\nProcess" -> "GStreamer" [ label="use library" ]
|
||||
"GStreamer" -> "Speakers" [ label="play audio" ]
|
||||
|
||||
|
||||
Thread/process communication
|
||||
============================
|
||||
|
||||
.. warning::
|
||||
This section is currently outdated.
|
||||
|
||||
- Everything starts with ``Main``.
|
||||
- ``Main`` creates a ``Core`` process which runs the frontend, backend, and
|
||||
mixer.
|
||||
- Mixers *may* create an additional process for communication with external
|
||||
devices, like ``NadTalker`` in this example.
|
||||
- Backend libraries *may* have threads of their own, like ``despotify`` here
|
||||
which has additional threads in the ``Core`` process.
|
||||
- ``Server`` part currently runs in the same process and thread as ``Main``.
|
||||
- ``Client`` is some external client talking to ``Server`` over a socket.
|
||||
|
||||
.. image:: /_static/thread_communication.png
|
||||
@ -26,7 +26,7 @@ Feature wishlist
|
||||
We maintain our collection of sane or less sane ideas for future Mopidy
|
||||
features as `issues <https://github.com/mopidy/mopidy/issues>`_ at GitHub
|
||||
labeled with `the "wishlist" label
|
||||
<https://github.com/mopidy/mopidy/issues/labels/wishlist>`_. Feel free to vote
|
||||
<https://github.com/mopidy/mopidy/issues?labels=wishlist>`_. Feel free to vote
|
||||
up any feature you would love to see in Mopidy, but please refrain from adding
|
||||
a comment just to say "I want this too!". You are of course free to add
|
||||
comments if you have suggestions for how the feature should work or be
|
||||
|
||||
@ -73,5 +73,12 @@ Using a custom audio sink
|
||||
=========================
|
||||
|
||||
If you for some reason want to use some other GStreamer audio sink than
|
||||
``autoaudiosink``, you can change :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
|
||||
in your ``settings.py`` file.
|
||||
``autoaudiosink``, you can add ``mopidy.outputs.custom.CustomOutput`` to the
|
||||
:attr:`mopidy.settings.OUTPUTS` setting, and set the
|
||||
:attr:`mopidy.settings.CUSTOM_OUTPUT` setting to a partial GStreamer pipeline
|
||||
description describing the GStreamer sink you want to use.
|
||||
|
||||
Example of ``settings.py`` for OSS4::
|
||||
|
||||
OUTPUTS = (u'mopidy.outputs.custom.CustomOutput',)
|
||||
CUSTOM_OUTPUT = u'oss4sink'
|
||||
|
||||
@ -9,8 +9,8 @@ setup and whether you want to use stable releases or less stable development
|
||||
versions.
|
||||
|
||||
|
||||
Install dependencies
|
||||
====================
|
||||
Requirements
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
@ -4,10 +4,11 @@
|
||||
|
||||
The following GStreamer audio outputs implements the :ref:`output-api`.
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs.custom
|
||||
.. autoclass:: mopidy.outputs.custom.CustomOutput
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs.local
|
||||
.. autoclass:: mopidy.outputs.local.LocalOutput
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs.shoutcast
|
||||
.. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput
|
||||
|
||||
@ -56,7 +56,7 @@ You may also want to change some of the ``LOCAL_*`` settings. See
|
||||
|
||||
Currently, Mopidy supports using Spotify *or* local storage as a music
|
||||
source. We're working on using both sources simultaneously, and will
|
||||
hopefully have support for this in the 0.3 release.
|
||||
hopefully have support for this in the 0.6 release.
|
||||
|
||||
|
||||
.. _generating_a_tag_cache:
|
||||
@ -92,6 +92,7 @@ To make a ``tag_cache`` of your local music available for Mopidy:
|
||||
|
||||
.. _use_mpd_on_a_network:
|
||||
|
||||
|
||||
Connecting from other machines on the network
|
||||
=============================================
|
||||
|
||||
@ -119,6 +120,31 @@ file::
|
||||
LASTFM_PASSWORD = u'mysecret'
|
||||
|
||||
|
||||
Streaming audio through a SHOUTcast/Icecast server
|
||||
==================================================
|
||||
|
||||
If you want to play the audio on another computer than the one running Mopidy,
|
||||
you can stream the audio from Mopidy through an SHOUTcast or Icecast audio
|
||||
streaming server. Multiple media players can then be connected to the streaming
|
||||
server simultaneously. To use the SHOUTcast output, do the following:
|
||||
|
||||
#. Install, configure and start the Icecast server. It can be found in the
|
||||
``icecast2`` package in Debian/Ubuntu.
|
||||
|
||||
#. Add ``mopidy.outputs.shoutcast.ShoutcastOutput`` output to the
|
||||
:attr:`mopidy.settings.OUTPUTS` setting.
|
||||
|
||||
#. Check the default values for the following settings, and alter them to match
|
||||
your Icecast setup if needed:
|
||||
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
|
||||
|
||||
|
||||
Available settings
|
||||
==================
|
||||
|
||||
|
||||
@ -263,6 +263,7 @@ class PlaybackController(object):
|
||||
.. digraph:: state_transitions
|
||||
|
||||
"STOPPED" -> "PLAYING" [ label="play" ]
|
||||
"STOPPED" -> "PAUSED" [ label="pause" ]
|
||||
"PLAYING" -> "STOPPED" [ label="stop" ]
|
||||
"PLAYING" -> "PAUSED" [ label="pause" ]
|
||||
"PLAYING" -> "PLAYING" [ label="play" ]
|
||||
|
||||
@ -22,7 +22,11 @@ class LocalBackend(ThreadingActor, Backend):
|
||||
"""
|
||||
A backend for playing music from a local music archive.
|
||||
|
||||
**Issues:** http://github.com/mopidy/mopidy/issues/labels/backend-local
|
||||
**Issues:** https://github.com/mopidy/mopidy/issues?labels=backend-local
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
@ -174,7 +178,7 @@ class LocalLibraryProvider(BaseLibraryProvider):
|
||||
|
||||
tracks = parse_mpd_tag_cache(tag_cache, music_folder)
|
||||
|
||||
logger.info('Loading songs in %s from %s', music_folder, tag_cache)
|
||||
logger.info('Loading tracks in %s from %s', music_folder, tag_cache)
|
||||
|
||||
for track in tracks:
|
||||
self._uri_mapping[track.uri] = track
|
||||
|
||||
@ -11,6 +11,7 @@ from mopidy.gstreamer import GStreamer
|
||||
logger = logging.getLogger('mopidy.backends.spotify')
|
||||
|
||||
ENCODING = 'utf-8'
|
||||
BITRATES = {96: 2, 160: 0, 320: 1}
|
||||
|
||||
class SpotifyBackend(ThreadingActor, Backend):
|
||||
"""
|
||||
@ -27,7 +28,12 @@ class SpotifyBackend(ThreadingActor, Backend):
|
||||
trade mark of the Spotify Group.
|
||||
|
||||
**Issues:**
|
||||
http://github.com/mopidy/mopidy/issues/labels/backend-spotify
|
||||
https://github.com/mopidy/mopidy/issues?labels=backend-spotify
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- libspotify == 0.0.8 (libspotify8 package from apt.mopidy.com)
|
||||
- pyspotify == 1.2 (python-spotify package from apt.mopidy.com)
|
||||
|
||||
**Settings:**
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import get_version, settings
|
||||
from mopidy.backends.base import Backend
|
||||
from mopidy.backends.spotify import BITRATES
|
||||
from mopidy.backends.spotify.translator import SpotifyTranslator
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.gstreamer import GStreamer
|
||||
@ -58,12 +59,10 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
return
|
||||
logger.info(u'Connected to Spotify')
|
||||
self.session = session
|
||||
if settings.SPOTIFY_HIGH_BITRATE:
|
||||
logger.debug(u'Preferring high bitrate from Spotify')
|
||||
self.session.set_preferred_bitrate(1)
|
||||
else:
|
||||
logger.debug(u'Preferring normal bitrate from Spotify')
|
||||
self.session.set_preferred_bitrate(0)
|
||||
|
||||
logger.debug(u'Preferred Spotify bitrate is %s kbps.', settings.SPOTIFY_BITRATE)
|
||||
self.session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE])
|
||||
|
||||
self.container_manager = SpotifyContainerManager(self)
|
||||
self.container_manager.watch(self.session.playlist_container())
|
||||
self.connected.set()
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.spotify import ENCODING
|
||||
from mopidy.backends.spotify import ENCODING, BITRATES
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.translator')
|
||||
@ -44,7 +44,7 @@ class SpotifyTranslator(object):
|
||||
track_no=spotify_track.index(),
|
||||
date=date,
|
||||
length=spotify_track.duration(),
|
||||
bitrate=(settings.SPOTIFY_HIGH_BITRATE and 320 or 160),
|
||||
bitrate=BITRATES[settings.SPOTIFY_BITRATE],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import optparse
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
@ -16,38 +17,44 @@ sys.argv[1:] = gstreamer_args
|
||||
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import get_version, settings, OptionalDependencyError
|
||||
from mopidy import (get_version, settings, OptionalDependencyError,
|
||||
SettingsError)
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy.utils import get_class
|
||||
from mopidy.utils.log import setup_logging
|
||||
from mopidy.utils.path import get_or_create_folder, get_or_create_file
|
||||
from mopidy.utils.process import GObjectEventThread
|
||||
from mopidy.utils.process import (GObjectEventThread, exit_handler,
|
||||
stop_all_actors)
|
||||
from mopidy.utils.settings import list_settings_optparse_callback
|
||||
|
||||
logger = logging.getLogger('mopidy.core')
|
||||
|
||||
def main():
|
||||
options = parse_options()
|
||||
setup_logging(options.verbosity_level, options.save_debug_log)
|
||||
setup_settings()
|
||||
setup_gobject_loop()
|
||||
setup_gstreamer()
|
||||
setup_mixer()
|
||||
setup_backend()
|
||||
setup_frontends()
|
||||
signal.signal(signal.SIGTERM, exit_handler)
|
||||
try:
|
||||
options = parse_options()
|
||||
setup_logging(options.verbosity_level, options.save_debug_log)
|
||||
setup_settings(options.interactive)
|
||||
setup_gobject_loop()
|
||||
setup_gstreamer()
|
||||
setup_mixer()
|
||||
setup_backend()
|
||||
setup_frontends()
|
||||
while ActorRegistry.get_all():
|
||||
time.sleep(1)
|
||||
logger.info(u'No actors left. Exiting...')
|
||||
except KeyboardInterrupt:
|
||||
logger.info(u'User interrupt. Exiting...')
|
||||
ActorRegistry.stop_all()
|
||||
stop_all_actors()
|
||||
|
||||
def parse_options():
|
||||
parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
|
||||
parser.add_option('--help-gst',
|
||||
action='store_true', dest='help_gst',
|
||||
help='show GStreamer help options')
|
||||
parser.add_option('-i', '--interactive',
|
||||
action='store_true', dest='interactive',
|
||||
help='ask interactively for required settings which is missing')
|
||||
parser.add_option('-q', '--quiet',
|
||||
action='store_const', const=0, dest='verbosity_level',
|
||||
help='less output (warning level)')
|
||||
@ -62,10 +69,14 @@ def parse_options():
|
||||
help='list current settings')
|
||||
return parser.parse_args(args=mopidy_args)[0]
|
||||
|
||||
def setup_settings():
|
||||
def setup_settings(interactive):
|
||||
get_or_create_folder('~/.mopidy/')
|
||||
get_or_create_file('~/.mopidy/settings.py')
|
||||
settings.validate()
|
||||
try:
|
||||
settings.validate(interactive)
|
||||
except SettingsError, e:
|
||||
logger.error(e.message)
|
||||
sys.exit(1)
|
||||
|
||||
def setup_gobject_loop():
|
||||
GObjectEventThread().start()
|
||||
|
||||
@ -13,11 +13,15 @@ class MpdFrontend(ThreadingActor, BaseFrontend):
|
||||
"""
|
||||
The MPD frontend.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`
|
||||
- :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
|
||||
- :attr:`mopidy.settings.MPD_SERVER_PORT`
|
||||
- :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@ -14,13 +14,10 @@ class MpdServer(asyncore.dispatcher):
|
||||
for each client connection.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
asyncore.dispatcher.__init__(self)
|
||||
|
||||
def start(self):
|
||||
"""Start MPD server."""
|
||||
try:
|
||||
self.socket = network.create_socket()
|
||||
self.set_socket(network.create_socket())
|
||||
self.set_reuse_addr()
|
||||
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||
port = settings.MPD_SERVER_PORT
|
||||
@ -39,7 +36,3 @@ class MpdServer(asyncore.dispatcher):
|
||||
logger.info(u'MPD client connection from [%s]:%s',
|
||||
client_socket_address[0], client_socket_address[1])
|
||||
MpdSession(self, client_socket, client_socket_address)
|
||||
|
||||
def handle_close(self):
|
||||
"""Called by asyncore when the socket is closed."""
|
||||
self.close()
|
||||
|
||||
@ -21,6 +21,14 @@ class CustomOutput(BaseOutput):
|
||||
these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a
|
||||
:command:`gst-launch` compatible string describing the target setup.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.CUSTOM_OUTPUT`
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
return settings.CUSTOM_OUTPUT
|
||||
|
||||
@ -6,6 +6,14 @@ class LocalOutput(BaseOutput):
|
||||
|
||||
This output will normally tell GStreamer to choose whatever it thinks is
|
||||
best for your system. In other words this is usually a sane choice.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- None
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
|
||||
@ -13,6 +13,19 @@ class ShoutcastOutput(BaseOutput):
|
||||
supports Shoutcast. The output supports setting for: server address, port,
|
||||
mount point, user, password and encoder to use. Please see
|
||||
:class:`mopidy.settings` for details about settings.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- A SHOUTcast/Icecast server
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
@ -21,9 +34,9 @@ class ShoutcastOutput(BaseOutput):
|
||||
|
||||
def modify_bin(self):
|
||||
self.set_properties(self.bin.get_by_name('shoutcast'), {
|
||||
u'ip': settings.SHOUTCAST_OUTPUT_SERVER,
|
||||
u'mount': settings.SHOUTCAST_OUTPUT_MOUNT,
|
||||
u'ip': settings.SHOUTCAST_OUTPUT_HOSTNAME,
|
||||
u'port': settings.SHOUTCAST_OUTPUT_PORT,
|
||||
u'mount': settings.SHOUTCAST_OUTPUT_MOUNT,
|
||||
u'username': settings.SHOUTCAST_OUTPUT_USERNAME,
|
||||
u'password': settings.SHOUTCAST_OUTPUT_PASSWORD,
|
||||
})
|
||||
|
||||
@ -24,7 +24,7 @@ def translator(data):
|
||||
_retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs)
|
||||
_retrieve(gst.TAG_ARTIST, 'name', artist_kwargs)
|
||||
|
||||
if gst.TAG_DATE in data:
|
||||
if gst.TAG_DATE in data and data[gst.TAG_DATE]:
|
||||
date = data[gst.TAG_DATE]
|
||||
date = datetime.date(date.year, date.month, date.day)
|
||||
track_kwargs['date'] = date
|
||||
@ -57,17 +57,16 @@ class Scanner(object):
|
||||
self.error_callback = error_callback
|
||||
self.loop = gobject.MainLoop()
|
||||
|
||||
caps = gst.Caps('audio/x-raw-int')
|
||||
fakesink = gst.element_factory_make('fakesink')
|
||||
pad = fakesink.get_pad('sink')
|
||||
|
||||
self.uribin = gst.element_factory_make('uridecodebin')
|
||||
self.uribin.connect('pad-added', self.process_new_pad, pad)
|
||||
self.uribin.set_property('caps', caps)
|
||||
self.uribin.set_property('caps', gst.Caps('audio/x-raw-int'))
|
||||
self.uribin.connect('pad-added', self.process_new_pad,
|
||||
fakesink.get_pad('sink'))
|
||||
|
||||
self.pipe = gst.element_factory_make('pipeline')
|
||||
self.pipe.add(fakesink)
|
||||
self.pipe.add(self.uribin)
|
||||
self.pipe.add(fakesink)
|
||||
|
||||
bus = self.pipe.get_bus()
|
||||
bus.add_signal_watch()
|
||||
@ -78,22 +77,36 @@ class Scanner(object):
|
||||
pad.link(target_pad)
|
||||
|
||||
def process_tags(self, bus, message):
|
||||
data = message.parse_tag()
|
||||
data = dict([(k, data[k]) for k in data.keys()])
|
||||
data['uri'] = unicode(self.uribin.get_property('uri'))
|
||||
data['duration'] = self.get_duration()
|
||||
self.data_callback(data)
|
||||
self.next_uri()
|
||||
taglist = message.parse_tag()
|
||||
data = {
|
||||
'uri': unicode(self.uribin.get_property('uri')),
|
||||
gst.TAG_DURATION: self.get_duration(),
|
||||
}
|
||||
|
||||
for key in taglist.keys():
|
||||
# XXX: For some crazy reason some wma files spit out lists here,
|
||||
# not sure if this is due to better data in headers or wma being
|
||||
# stupid. So ugly hack for now :/
|
||||
if type(taglist[key]) is list:
|
||||
data[key] = taglist[key][0]
|
||||
else:
|
||||
data[key] = taglist[key]
|
||||
|
||||
try:
|
||||
self.data_callback(data)
|
||||
self.next_uri()
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
|
||||
def process_error(self, bus, message):
|
||||
if self.error_callback:
|
||||
uri = self.uribin.get_property('uri')
|
||||
errors = message.parse_error()
|
||||
self.error_callback(uri, errors)
|
||||
error, debug = message.parse_error()
|
||||
self.error_callback(uri, error, debug)
|
||||
self.next_uri()
|
||||
|
||||
def get_duration(self):
|
||||
self.pipe.get_state()
|
||||
self.pipe.get_state() # Block until state change is done.
|
||||
try:
|
||||
return self.pipe.query_duration(
|
||||
gst.FORMAT_TIME, None)[0] // gst.MSECOND
|
||||
|
||||
@ -158,16 +158,16 @@ MIXER_MAX_VOLUME = 100
|
||||
#: Listens on all interfaces, both IPv4 and IPv6.
|
||||
MPD_SERVER_HOSTNAME = u'127.0.0.1'
|
||||
|
||||
#: The password required for connecting to the MPD server.
|
||||
#:
|
||||
#: Default: :class:`None`, which means no password required.
|
||||
MPD_SERVER_PASSWORD = None
|
||||
|
||||
#: Which TCP port Mopidy's MPD server should listen to.
|
||||
#:
|
||||
#: Default: 6600
|
||||
MPD_SERVER_PORT = 6600
|
||||
|
||||
#: The password required for connecting to the MPD server.
|
||||
#:
|
||||
#: Default: :class:`None`, which means no password required.
|
||||
MPD_SERVER_PASSWORD = None
|
||||
|
||||
#: List of outputs to use. See :mod:`mopidy.outputs` for all available
|
||||
#: backends
|
||||
#:
|
||||
@ -180,42 +180,54 @@ OUTPUTS = (
|
||||
u'mopidy.outputs.local.LocalOutput',
|
||||
)
|
||||
|
||||
#: Servar that runs Shoutcast server to send stream to.
|
||||
#: Hostname of the SHOUTcast server which Mopidy should stream audio to.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_SERVER = u'127.0.0.1'
|
||||
SHOUTCAST_OUTPUT_SERVER = u'127.0.0.1'
|
||||
#: SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1'
|
||||
SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1'
|
||||
|
||||
#: User to authenticate as against Shoutcast server.
|
||||
#: Port of the SHOUTcast server.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_USERNAME = u'source'
|
||||
SHOUTCAST_OUTPUT_USERNAME = u'source'
|
||||
|
||||
#: Password to authenticate with against Shoutcast server.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
|
||||
SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
|
||||
|
||||
#: Port to use for streaming to Shoutcast server.
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_PORT = 8000
|
||||
SHOUTCAST_OUTPUT_PORT = 8000
|
||||
|
||||
#: Mountpoint to use for the stream on the Shoutcast server.
|
||||
#: User to authenticate as against SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_USERNAME = u'source'
|
||||
SHOUTCAST_OUTPUT_USERNAME = u'source'
|
||||
|
||||
#: Password to authenticate with against SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
|
||||
SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
|
||||
|
||||
#: Mountpoint to use for the stream on the SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_MOUNT = u'/stream'
|
||||
SHOUTCAST_OUTPUT_MOUNT = u'/stream'
|
||||
|
||||
#: Encoder to use to process audio data before streaming.
|
||||
#: Encoder to use to process audio data before streaming to SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
@ -237,11 +249,13 @@ SPOTIFY_USERNAME = u''
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
SPOTIFY_PASSWORD = u''
|
||||
|
||||
#: Do you prefer high bitrate (320k)?
|
||||
#: Spotify preferred bitrate.
|
||||
#:
|
||||
#: Available values are 96, 160, and 320.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
#
|
||||
#: Default::
|
||||
#:
|
||||
#: SPOTIFY_HIGH_BITRATE = False # 160k
|
||||
SPOTIFY_HIGH_BITRATE = False
|
||||
#: SPOTIFY_BITRATE = 160
|
||||
SPOTIFY_BITRATE = 160
|
||||
|
||||
@ -1,15 +1,30 @@
|
||||
import logging
|
||||
import signal
|
||||
import threading
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
from pykka import ActorDeadError
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import SettingsError
|
||||
|
||||
logger = logging.getLogger('mopidy.utils.process')
|
||||
|
||||
def exit_handler(signum, frame):
|
||||
"""A :mod:`signal` handler which will exit the program on signal."""
|
||||
signals = dict((k, v) for v, k in signal.__dict__.iteritems()
|
||||
if v.startswith('SIG') and not v.startswith('SIG_'))
|
||||
logger.info(u'Got %s. Exiting...', signals[signum])
|
||||
stop_all_actors()
|
||||
|
||||
def stop_all_actors():
|
||||
num_actors = len(ActorRegistry.get_all())
|
||||
while num_actors:
|
||||
logger.debug(u'Stopping %d actor(s)...', num_actors)
|
||||
ActorRegistry.stop_all()
|
||||
num_actors = len(ActorRegistry.get_all())
|
||||
|
||||
class BaseThread(threading.Thread):
|
||||
def __init__(self):
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# Absolute import needed to import ~/.mopidy/settings.py and not ourselves
|
||||
from __future__ import absolute_import
|
||||
from copy import copy
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
from pprint import pformat
|
||||
@ -63,12 +64,28 @@ class SettingsProxy(object):
|
||||
else:
|
||||
super(SettingsProxy, self).__setattr__(attr, value)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, interactive):
|
||||
if interactive:
|
||||
self._read_missing_settings_from_stdin(self.current, self.runtime)
|
||||
if self.get_errors():
|
||||
logger.error(u'Settings validation errors: %s',
|
||||
indent(self.get_errors_as_string()))
|
||||
raise SettingsError(u'Settings validation failed.')
|
||||
|
||||
def _read_missing_settings_from_stdin(self, current, runtime):
|
||||
for setting, value in current.iteritems():
|
||||
if isinstance(value, basestring) and len(value) == 0:
|
||||
runtime[setting] = self._read_from_stdin(setting + u': ')
|
||||
|
||||
def _read_from_stdin(self, prompt):
|
||||
if u'_PASSWORD' in prompt:
|
||||
return (getpass.getpass(prompt)
|
||||
.decode(sys.stdin.encoding, 'ignore'))
|
||||
else:
|
||||
sys.stdout.write(prompt)
|
||||
return (sys.stdin.readline().strip()
|
||||
.decode(sys.stdin.encoding, 'ignore'))
|
||||
|
||||
def get_errors(self):
|
||||
return validate_settings(self.default, self.local)
|
||||
|
||||
@ -107,6 +124,7 @@ def validate_settings(defaults, settings):
|
||||
'SERVER': None,
|
||||
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
|
||||
'SERVER_PORT': 'MPD_SERVER_PORT',
|
||||
'SPOTIFY_HIGH_BITRATE': 'SPOTIFY_BITRATE',
|
||||
'SPOTIFY_LIB_APPKEY': None,
|
||||
'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH',
|
||||
}
|
||||
@ -127,6 +145,11 @@ def validate_settings(defaults, settings):
|
||||
'longer available.')
|
||||
continue
|
||||
|
||||
if setting == 'SPOTIFY_BITRATE':
|
||||
if value not in (96, 160, 320):
|
||||
errors[setting] = (u'Unavailable Spotify bitrate. ' +
|
||||
u'Available bitrates are 96, 160, and 320.')
|
||||
|
||||
if setting not in defaults:
|
||||
errors[setting] = u'Unknown setting. Is it misspelled?'
|
||||
continue
|
||||
|
||||
5
setup.py
5
setup.py
@ -69,11 +69,6 @@ for dirpath, dirnames, filenames in os.walk(project_dir):
|
||||
data_files.append([dirpath,
|
||||
[os.path.join(dirpath, f) for f in filenames]])
|
||||
|
||||
if os.geteuid() == 0:
|
||||
# Only try to install this file if we are root
|
||||
data_files.append(
|
||||
('/usr/local/share/applications', ['data/mopidy.desktop']))
|
||||
|
||||
setup(
|
||||
name='Mopidy',
|
||||
version=get_version(),
|
||||
|
||||
@ -14,6 +14,7 @@ class HelpTest(unittest.TestCase):
|
||||
self.assert_('--version' in output)
|
||||
self.assert_('--help' in output)
|
||||
self.assert_('--help-gst' in output)
|
||||
self.assert_('--interactive' in output)
|
||||
self.assert_('--quiet' in output)
|
||||
self.assert_('--verbose' in output)
|
||||
self.assert_('--save-debug-log' in output)
|
||||
|
||||
@ -4,7 +4,7 @@ from datetime import date
|
||||
from mopidy.scanner import Scanner, translator
|
||||
from mopidy.models import Track, Artist, Album
|
||||
|
||||
from tests import path_to_data_dir
|
||||
from tests import path_to_data_dir, SkipTest
|
||||
|
||||
class FakeGstDate(object):
|
||||
def __init__(self, year, month, day):
|
||||
@ -144,9 +144,9 @@ class ScannerTest(unittest.TestCase):
|
||||
uri = data['uri'][len('file://'):]
|
||||
self.data[uri] = data
|
||||
|
||||
def error_callback(self, uri, errors):
|
||||
def error_callback(self, uri, error, debug):
|
||||
uri = uri[len('file://'):]
|
||||
self.errors[uri] = errors
|
||||
self.errors[uri] = (error, debug)
|
||||
|
||||
def test_data_is_set(self):
|
||||
self.scan('scanner/simple')
|
||||
@ -184,3 +184,7 @@ class ScannerTest(unittest.TestCase):
|
||||
def test_other_media_is_ignored(self):
|
||||
self.scan('scanner/image')
|
||||
self.assert_(self.errors)
|
||||
|
||||
@SkipTest
|
||||
def test_song_without_time_is_handeled(self):
|
||||
pass
|
||||
|
||||
@ -10,6 +10,7 @@ class ValidateSettingsTest(unittest.TestCase):
|
||||
self.defaults = {
|
||||
'MPD_SERVER_HOSTNAME': '::',
|
||||
'MPD_SERVER_PORT': 6600,
|
||||
'SPOTIFY_BITRATE': 160,
|
||||
}
|
||||
|
||||
def test_no_errors_yields_empty_dict(self):
|
||||
@ -42,6 +43,13 @@ class ValidateSettingsTest(unittest.TestCase):
|
||||
'"mopidy.backends.despotify.DespotifyBackend" is no longer ' +
|
||||
'available.')
|
||||
|
||||
def test_unavailable_bitrate_setting_returns_error(self):
|
||||
result = validate_settings(self.defaults,
|
||||
{'SPOTIFY_BITRATE': 50})
|
||||
self.assertEqual(result['SPOTIFY_BITRATE'],
|
||||
u'Unavailable Spotify bitrate. ' +
|
||||
u'Available bitrates are 96, 160, and 320.')
|
||||
|
||||
def test_two_errors_are_both_reported(self):
|
||||
result = validate_settings(self.defaults,
|
||||
{'FOO': '', 'BAR': ''})
|
||||
@ -63,6 +71,7 @@ class ValidateSettingsTest(unittest.TestCase):
|
||||
class SettingsProxyTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.settings = SettingsProxy(default_settings_module)
|
||||
self.settings.local.clear()
|
||||
|
||||
def test_set_and_get_attr(self):
|
||||
self.settings.TEST = 'test'
|
||||
@ -141,6 +150,20 @@ class SettingsProxyTest(unittest.TestCase):
|
||||
actual = self.settings.TEST
|
||||
self.assertEqual(actual, './test')
|
||||
|
||||
def test_interactive_input_of_missing_defaults(self):
|
||||
self.settings.default['TEST'] = ''
|
||||
interactive_input = 'input'
|
||||
self.settings._read_from_stdin = lambda _: interactive_input
|
||||
self.settings.validate(interactive=True)
|
||||
self.assertEqual(interactive_input, self.settings.TEST)
|
||||
|
||||
def test_interactive_input_not_needed_when_setting_is_set_locally(self):
|
||||
self.settings.default['TEST'] = ''
|
||||
self.settings.local['TEST'] = 'test'
|
||||
self.settings._read_from_stdin = lambda _: self.fail(
|
||||
'Should not read from stdin')
|
||||
self.settings.validate(interactive=True)
|
||||
|
||||
|
||||
class FormatSettingListTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user