fix playlist sync

This commit is contained in:
blushingpenguin 2021-07-11 19:52:34 +01:00
parent 58ef73b3e6
commit e09f5aecad
6 changed files with 108 additions and 89 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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():