fix playlist sync
This commit is contained in:
parent
58ef73b3e6
commit
e09f5aecad
|
@ -24,6 +24,9 @@ import gpodder
|
|||
from gpodder import util
|
||||
from gpodder.sync import (episode_filename_on_device,
|
||||
episode_foldername_on_device)
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import GLib, Gio
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
|
@ -36,12 +39,19 @@ class gPodderDevicePlaylist(object):
|
|||
self._config = config
|
||||
self.linebreak = '\r\n'
|
||||
self.playlist_file = util.sanitize_filename(playlist_name, self._config.device_sync.max_filename_length) + '.m3u'
|
||||
self.playlist_folder = os.path.join(self._config.device_sync.device_folder, self._config.device_sync.playlists.folder)
|
||||
self.mountpoint = util.find_mount_point(self.playlist_folder)
|
||||
if self.mountpoint == '/':
|
||||
device_folder = util.new_gio_file(self._config.device_sync.device_folder)
|
||||
self.playlist_folder = device_folder.resolve_relative_path(self._config.device_sync.playlists.folder)
|
||||
|
||||
self.mountpoint = None
|
||||
try:
|
||||
self.mountpoint = self.playlist_folder.find_enclosing_mount().get_root()
|
||||
except GLib.Error as err:
|
||||
logger.error('find_enclosing_mount folder %s failed: %s', self.playlist_folder.get_uri(), err.message)
|
||||
|
||||
if not self.mountpoint:
|
||||
self.mountpoint = self.playlist_folder
|
||||
logger.warning('MP3 player resides on / - using %s as MP3 player root', self.mountpoint)
|
||||
self.playlist_absolute_filename = os.path.join(self.playlist_folder, self.playlist_file)
|
||||
logger.warning('could not find mount point for MP3 player - using %s as MP3 player root', self.mountpoint.get_uri())
|
||||
self.playlist_absolute_filename = self.playlist_folder.resolve_relative_path(self.playlist_file)
|
||||
|
||||
def build_extinf(self, filename):
|
||||
# TODO: Windows playlists
|
||||
|
@ -64,11 +74,16 @@ class gPodderDevicePlaylist(object):
|
|||
read all files from the existing playlist
|
||||
"""
|
||||
tracks = []
|
||||
logger.info("Read data from the playlistfile %s" % self.playlist_absolute_filename)
|
||||
if os.path.exists(self.playlist_absolute_filename):
|
||||
for line in open(self.playlist_absolute_filename, 'r'):
|
||||
logger.info("Read data from the playlistfile %s" % self.playlist_absolute_filename.get_uri())
|
||||
if self.playlist_absolute_filename.query_exists():
|
||||
stream = Gio.DataInputStream.new(self.playlist_absolute_filename.read())
|
||||
while True:
|
||||
line = stream.read_line_utf8()[0]
|
||||
if not line:
|
||||
break
|
||||
if not line.startswith('#EXT'):
|
||||
tracks.append(line.rstrip('\r\n'))
|
||||
stream.close()
|
||||
return tracks
|
||||
|
||||
def get_filename_for_playlist(self, episode):
|
||||
|
@ -86,7 +101,7 @@ class gPodderDevicePlaylist(object):
|
|||
if foldername:
|
||||
filename = os.path.join(foldername, filename)
|
||||
if self._config.device_sync.playlist.absolute_path:
|
||||
filename = os.path.join(util.relpath(self.mountpoint, self._config.device_sync.device_folder), filename)
|
||||
filename = os.path.join(util.relpath(self._config.device_sync.device_folder, self.mountpoint.get_uri()), filename)
|
||||
return filename
|
||||
|
||||
def write_m3u(self, episodes):
|
||||
|
@ -97,12 +112,12 @@ class gPodderDevicePlaylist(object):
|
|||
if not util.make_directory(self.playlist_folder):
|
||||
raise IOError(_('Folder %s could not be created.') % self.playlist_folder, _('Error writing playlist'))
|
||||
else:
|
||||
fp = open(os.path.join(self.playlist_folder, self.playlist_file), 'w')
|
||||
fp.write('#EXTM3U%s' % self.linebreak)
|
||||
os = Gio.DataOutputStream.new(self.playlist_absolute_filename.replace(None, False, Gio.FileCreateFlags.NONE))
|
||||
os.put_string('#EXTM3U%s' % self.linebreak)
|
||||
for current_episode in episodes:
|
||||
filename = self.get_filename_for_playlist(current_episode)
|
||||
fp.write(self.build_extinf(filename))
|
||||
os.put_string(self.build_extinf(filename))
|
||||
filename = self.get_absolute_filename_for_playlist(current_episode)
|
||||
fp.write(filename)
|
||||
fp.write(self.linebreak)
|
||||
fp.close()
|
||||
os.put_string(filename)
|
||||
os.put_string(self.linebreak)
|
||||
os.close()
|
||||
|
|
|
@ -26,7 +26,7 @@ from gi.repository import Gdk, Gtk, Pango
|
|||
import gpodder
|
||||
from gpodder import util, vimeo, youtube
|
||||
from gpodder.gtkui.desktopfile import PlayerListModel
|
||||
from gpodder.gtkui.interface.common import BuilderWidget, TreeViewHelper
|
||||
from gpodder.gtkui.interface.common import BuilderWidget, TreeViewHelper, show_message_dialog
|
||||
from gpodder.gtkui.interface.configeditor import gPodderConfigEditor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -688,12 +688,21 @@ class gPodderPreferences(BuilderWidget):
|
|||
def on_btn_playlist_folder_clicked(self, widget):
|
||||
fs = Gtk.FileChooserDialog(title=_('Select folder for playlists'),
|
||||
action=Gtk.FileChooserAction.SELECT_FOLDER)
|
||||
fs.set_local_only(False)
|
||||
fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
fs.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||
fs.set_current_folder(self.btn_playlistfolder.get_label())
|
||||
if fs.run() == Gtk.ResponseType.OK:
|
||||
filename = util.relpath(self._config.device_sync.device_folder,
|
||||
fs.get_filename())
|
||||
|
||||
device_folder = util.new_gio_file(self._config.device_sync.device_folder)
|
||||
playlists_folder = device_folder.resolve_relative_path(self._config.device_sync.playlists.folder)
|
||||
fs.set_file(playlists_folder)
|
||||
|
||||
while fs.run() == Gtk.ResponseType.OK:
|
||||
filename = util.relpath(fs.get_uri(),
|
||||
self._config.device_sync.device_folder)
|
||||
if not filename:
|
||||
show_message_dialog(fs, _('The playlists folder must be on the device'))
|
||||
continue
|
||||
|
||||
if self._config.device_sync.device_type == 'filesystem':
|
||||
self._config.device_sync.playlists.folder = filename
|
||||
self.btn_playlistfolder.set_label(filename or "")
|
||||
|
@ -701,5 +710,6 @@ class gPodderPreferences(BuilderWidget):
|
|||
if children:
|
||||
label = children.pop()
|
||||
label.set_alignment(0., .5)
|
||||
break
|
||||
|
||||
fs.destroy()
|
||||
|
|
|
@ -29,6 +29,21 @@ from gpodder.gtkui.base import GtkBuilderWidget
|
|||
_ = gpodder.gettext
|
||||
|
||||
|
||||
def show_message_dialog(parent, message, title=None):
|
||||
dlg = Gtk.MessageDialog(parent, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.OK)
|
||||
if title:
|
||||
dlg.set_title(str(title))
|
||||
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
|
||||
else:
|
||||
dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
|
||||
# make message copy/pastable
|
||||
for lbl in dlg.get_message_area():
|
||||
if isinstance(lbl, Gtk.Label):
|
||||
lbl.set_selectable(True)
|
||||
dlg.run()
|
||||
dlg.destroy()
|
||||
|
||||
|
||||
class BuilderWidget(GtkBuilderWidget):
|
||||
def __init__(self, parent, **kwargs):
|
||||
self._window_iconified = False
|
||||
|
@ -64,18 +79,7 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
|
||||
def show_message(self, message, title=None, important=False, widget=None):
|
||||
if important:
|
||||
dlg = Gtk.MessageDialog(self.main_window, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.OK)
|
||||
if title:
|
||||
dlg.set_title(str(title))
|
||||
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
|
||||
else:
|
||||
dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
|
||||
# make message copy/pastable
|
||||
for lbl in dlg.get_message_area():
|
||||
if isinstance(lbl, Gtk.Label):
|
||||
lbl.set_selectable(True)
|
||||
dlg.run()
|
||||
dlg.destroy()
|
||||
show_message_dialog(self.main_window, message, title)
|
||||
else:
|
||||
gpodder.user_extensions.on_notification_show(title, message)
|
||||
|
||||
|
|
|
@ -503,13 +503,6 @@ class iPodDevice(Device):
|
|||
except:
|
||||
logger.warning('Seems like your python-gpod is out-of-date.')
|
||||
|
||||
def is_url(url):
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
return not not parsed.scheme
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
class MP3PlayerDevice(Device):
|
||||
def __init__(self, config,
|
||||
download_status_model,
|
||||
|
@ -518,10 +511,7 @@ class MP3PlayerDevice(Device):
|
|||
Device.__init__(self, config)
|
||||
|
||||
folder = self._config.device_sync.device_folder
|
||||
if is_url(folder):
|
||||
self.destination = Gio.File.new_for_uri(folder)
|
||||
else:
|
||||
self.destination = Gio.File.new_for_path(folder)
|
||||
self.destination = util.new_gio_file(folder)
|
||||
self.mount_volume_for_file = mount_volume_for_file
|
||||
self.download_status_model = download_status_model
|
||||
self.download_queue_manager = download_queue_manager
|
||||
|
@ -599,17 +589,9 @@ class MP3PlayerDevice(Device):
|
|||
to_file = self.get_episode_file_on_device(episode)
|
||||
to_file = folder.get_child(to_file)
|
||||
|
||||
if not folder.query_exists(None):
|
||||
try:
|
||||
folder.make_directory_with_parents(None)
|
||||
except GLib.Error as err:
|
||||
# The sync might be multithreaded, so directories can be created by other threads
|
||||
if not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.EXISTS):
|
||||
logger.error('Cannot create folder %s on MP3 player: %s', folder.get_uri(), err.message)
|
||||
self.cancel()
|
||||
return False
|
||||
util.make_directory(folder)
|
||||
|
||||
if not to_file.query_exists(None):
|
||||
if not to_file.query_exists():
|
||||
logger.info('Copying %s => %s',
|
||||
os.path.basename(from_file),
|
||||
to_file.get_uri())
|
||||
|
@ -674,9 +656,9 @@ class MP3PlayerDevice(Device):
|
|||
# get the folder on the device
|
||||
file = Gio.File.new_for_uri(track.filename)
|
||||
folder = file.get_parent()
|
||||
if file.query_exists(None):
|
||||
if file.query_exists():
|
||||
try:
|
||||
file.delete(None)
|
||||
file.delete()
|
||||
except GLib.Error as err:
|
||||
# if the file went away don't worry about it
|
||||
if not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND):
|
||||
|
@ -686,7 +668,7 @@ class MP3PlayerDevice(Device):
|
|||
if self._config.one_folder_per_podcast:
|
||||
try:
|
||||
if self.directory_is_empty(folder):
|
||||
folder.delete(None)
|
||||
folder.delete()
|
||||
except GLib.Error as err:
|
||||
# if the folder went away don't worry about it (multiple threads could
|
||||
# make this happen if they both notice the folder is empty simultaneously)
|
||||
|
|
|
@ -249,9 +249,8 @@ class gPodderSyncUI(object):
|
|||
# if playlist doesn't exist (yet) episodes_in_playlist will be empty
|
||||
if episodes_in_playlists:
|
||||
for episode_filename in episodes_in_playlists:
|
||||
|
||||
if not(os.path.exists(os.path.join(playlist.mountpoint,
|
||||
episode_filename))):
|
||||
if not playlist.mountpoint.resolve_relative_path(
|
||||
episode_filename).query_exists():
|
||||
# episode was synced but no longer on device
|
||||
# i.e. must have been deleted by user, so delete from gpodder
|
||||
try:
|
||||
|
|
|
@ -158,20 +158,46 @@ _MIME_TYPES = dict((k, v) for v, k in _MIME_TYPE_LIST)
|
|||
_MIME_TYPES_EXT = dict(_MIME_TYPE_LIST)
|
||||
|
||||
|
||||
def is_absolute_url(url):
|
||||
"""
|
||||
Check if url is an absolute url (i.e. has a scheme)
|
||||
"""
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
return not not parsed.scheme
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def new_gio_file(path):
|
||||
"""
|
||||
Create a new Gio.File given a path or uri
|
||||
"""
|
||||
if is_absolute_url(path):
|
||||
return Gio.File.new_for_uri(path)
|
||||
else:
|
||||
return Gio.File.new_for_path(path)
|
||||
|
||||
|
||||
def make_directory(path):
|
||||
"""
|
||||
Tries to create a directory if it does not exist already.
|
||||
Returns True if the directory exists after the function
|
||||
call, False otherwise.
|
||||
"""
|
||||
if os.path.isdir(path):
|
||||
if not isinstance(path, Gio.File):
|
||||
path = new_gio_file(path)
|
||||
|
||||
if path.query_exists():
|
||||
return True
|
||||
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except:
|
||||
logger.warn('Could not create directory: %s', path)
|
||||
return False
|
||||
path.make_directory_with_parents()
|
||||
except GLib.Error as err:
|
||||
# The sync might be multithreaded, so directories can be created by other threads
|
||||
if not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.EXISTS):
|
||||
logger.warn('Could not create directory %s: %s', path.get_uri(), err.message)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
@ -1626,34 +1652,17 @@ def isabs(string):
|
|||
return os.path.isabs(string)
|
||||
|
||||
|
||||
def commonpath(l1, l2, common=[]):
|
||||
"""
|
||||
helper functions for relpath
|
||||
Source: http://code.activestate.com/recipes/208993/
|
||||
"""
|
||||
if len(l1) < 1: return (common, l1, l2)
|
||||
if len(l2) < 1: return (common, l1, l2)
|
||||
if l1[0] != l2[0]: return (common, l1, l2)
|
||||
return commonpath(l1[1:], l2[1:], common + [l1[0]])
|
||||
|
||||
|
||||
def relpath(p1, p2):
|
||||
"""
|
||||
Finds relative path from p1 to p2
|
||||
Source: http://code.activestate.com/recipes/208993/
|
||||
Finds relative path from p2 to p1, like os.path.relpath but handles
|
||||
uris. Returns None if no such path exists due to the paths being on
|
||||
different devices.
|
||||
"""
|
||||
def pathsplit(s):
|
||||
return s.split(os.path.sep)
|
||||
|
||||
(common, l1, l2) = commonpath(pathsplit(p1), pathsplit(p2))
|
||||
p = []
|
||||
if len(l1) > 0:
|
||||
p = [('..' + os.sep) * len(l1)]
|
||||
p = p + l2
|
||||
if len(p) == 0:
|
||||
return "."
|
||||
|
||||
return os.path.join(*p)
|
||||
u1 = urllib.parse.urlparse(p1)
|
||||
u2 = urllib.parse.urlparse(p2)
|
||||
if u1.scheme and u2.scheme and (u1.scheme != u2.scheme or u1.netloc != u2.netloc):
|
||||
return None
|
||||
return os.path.relpath(u1.path, u2.path)
|
||||
|
||||
|
||||
def get_hostname():
|
||||
|
|
Loading…
Reference in New Issue