Skip to content

Commit

Permalink
Merge pull request #1054 from blushingpenguin/master
Browse files Browse the repository at this point in the history
use Gio for file system based device sync (allows mtp:// urls)
  • Loading branch information
elelay committed Jul 23, 2021
2 parents 3a4915c + c9bbc4f commit 99c46e8
Show file tree
Hide file tree
Showing 14 changed files with 442 additions and 524 deletions.
10 changes: 9 additions & 1 deletion bin/gpo
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,13 @@ class gPodderCli(object):
def _not_applicable(*args, **kwargs):
pass

def _mount_volume_for_file(file):
result, message = util.mount_volume_for_file(file, None)
if not result:
self._error(_('mounting volume for file %(file)s failed with: %(error)s'
% dict(file=file.get_uri(), error=message)))
return result

class DownloadStatusModel(object):
def register_task(self, ask):
pass
Expand All @@ -961,7 +968,8 @@ class gPodderCli(object):
_not_applicable,
self._db.commit,
_delete_episode_list,
_episode_selector)
_episode_selector,
_mount_volume_for_file)
done_lock.acquire()
sync_ui.on_synchronize_episodes(self._model.get_podcasts(), episodes=None, force_played=True, done_callback=done_lock.release)
done_lock.acquire() # block until done
Expand Down
2 changes: 1 addition & 1 deletion share/gpodder/extensions/ubuntu_unity.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@ def on_unload(self):
self.launcher_entry = None

def on_download_progress(self, progress):
GObject.idle_add(self.launcher_entry.set_progress, float(value))
GObject.idle_add(self.launcher_entry.set_progress, float(progress))
8 changes: 8 additions & 0 deletions share/gpodder/ui/gtk/gpodderpreferences.ui
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,14 @@
<property name="draw-indicator">True</property>
</object>
</child>
<child>
<object class="GtkCheckButton" id="checkbutton_delete_deleted_episodes">
<property name="label" translatable="yes">Remove episodes deleted in gPodder from device</property>
<property name="visible">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
</object>
</child>
</object>
<packing>
<property name="tab-label" translatable="yes">Devices</property>
Expand Down
1 change: 1 addition & 0 deletions src/gpodder/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
'one_folder_per_podcast': True,
'skip_played_episodes': True,
'delete_played_episodes': False,
'delete_deleted_episodes': False,

'max_filename_length': 120,

Expand Down
62 changes: 47 additions & 15 deletions src/gpodder/deviceplaylist.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
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

Expand All @@ -36,12 +39,19 @@ def __init__(self, config, playlist_name):
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
Expand All @@ -64,11 +74,16 @@ def read_m3u(self):
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):
Expand All @@ -86,7 +101,7 @@ def get_absolute_filename_for_playlist(self, episode):
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):
Expand All @@ -97,12 +112,29 @@ def write_m3u(self, episodes):
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)
# work around libmtp devices potentially having limited capabilities for partial writes
is_mtp = self.playlist_folder.get_uri().startswith("mtp://")
tempfile = None
if is_mtp:
tempfile = Gio.File.new_tmp()
fs = tempfile[1].get_output_stream()
else:
fs = self.playlist_absolute_filename.replace(None, False, Gio.FileCreateFlags.NONE)

os = Gio.DataOutputStream.new(fs)
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()

if is_mtp:
try:
tempfile[0].copy(self.playlist_absolute_filename, Gio.FileCopyFlags.OVERWRITE)
except GLib.Error as err:
logger.error('copying playlist to mtp device file %s failed: %s',
self.playlist_absolute_filename.get_uri(), err.message)
tempfile[0].delete()
18 changes: 9 additions & 9 deletions src/gpodder/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,14 +383,14 @@ def run(self):
if not self.continue_check_callback(self):
return

try:
task = self.queue.get_next()
logger.info('%s is processing: %s', self, task)
task.run()
task.recycle()
except StopIteration as e:
task = self.queue.get_next()
if not task:
logger.info('No more tasks for %s to carry out.', self)
break
logger.info('%s is processing: %s', self, task)
task.run()
task.recycle()

self.exit_callback(self)


Expand Down Expand Up @@ -439,8 +439,9 @@ def __spawn_threads(self):
spawn_limit = max_downloads - len(self.worker_threads)
else:
spawn_limit = self._config.limit.downloads.concurrent_max
logger.info('%r tasks to do, can start at most %r threads', work_count, spawn_limit)
for i in range(0, min(work_count, spawn_limit)):
running = len(self.worker_threads)
logger.info('%r tasks to do, can start at most %r threads, %r threads currently running', work_count, spawn_limit, running)
for i in range(0, min(work_count, spawn_limit - running)):
# We have to create a new thread here, there's work to do
logger.info('Starting new worker thread.')

Expand All @@ -460,7 +461,6 @@ def force_start_task(self, task):
def queue_task(self, task):
"""Marks a task as queued
"""
task.status = DownloadTask.QUEUED
self.__spawn_threads()


Expand Down
40 changes: 25 additions & 15 deletions src/gpodder/gtkui/desktop/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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__)
Expand Down Expand Up @@ -293,6 +293,8 @@ def new(self):
self.checkbutton_create_playlists)
self._config.connect_gtk_togglebutton('device_sync.playlists.two_way_sync',
self.checkbutton_delete_using_playlists)
self._config.connect_gtk_togglebutton('device_sync.delete_deleted_episodes',
self.checkbutton_delete_deleted_episodes)

# Have to do this before calling set_active on checkbutton_enable
self._enable_mygpo = self._config.mygpo.enabled
Expand Down Expand Up @@ -640,7 +642,7 @@ def on_combobox_device_type_changed(self, widget):
self.combobox_on_sync.set_sensitive(False)
self.checkbutton_skip_played_episodes.set_sensitive(False)
elif device_type == 'filesystem':
self.btn_filesystemMountpoint.set_label(self._config.device_sync.device_folder)
self.btn_filesystemMountpoint.set_label(self._config.device_sync.device_folder or "")
self.btn_filesystemMountpoint.set_sensitive(True)
self.checkbutton_create_playlists.set_sensitive(True)
children = self.btn_filesystemMountpoint.get_children()
Expand All @@ -650,6 +652,7 @@ def on_combobox_device_type_changed(self, widget):
self.toggle_playlist_interface(self._config.device_sync.playlists.create)
self.combobox_on_sync.set_sensitive(True)
self.checkbutton_skip_played_episodes.set_sensitive(True)
self.checkbutton_delete_deleted_episodes.set_sensitive(True)
elif device_type == 'ipod':
self.btn_filesystemMountpoint.set_label(self._config.device_sync.device_folder)
self.btn_filesystemMountpoint.set_sensitive(True)
Expand All @@ -664,22 +667,19 @@ def on_combobox_device_type_changed(self, widget):
label = children.pop()
label.set_alignment(0., .5)

else:
# TODO: Add support for iPod and MTP devices
pass

def on_btn_device_mountpoint_clicked(self, widget):
fs = Gtk.FileChooserDialog(title=_('Select folder for mount point'),
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_filesystemMountpoint.get_label())

fs.set_uri(self.btn_filesystemMountpoint.get_label() or "")
if fs.run() == Gtk.ResponseType.OK:
filename = fs.get_filename()
if self._config.device_sync.device_type == 'filesystem':
self._config.device_sync.device_folder = filename
self._config.device_sync.device_folder = fs.get_uri()
elif self._config.device_sync.device_type == 'ipod':
self._config.device_sync.device_folder = filename
self._config.device_sync.device_folder = fs.get_filename()
# Request an update of the mountpoint button
self.on_combobox_device_type_changed(None)

Expand All @@ -688,18 +688,28 @@ def on_btn_device_mountpoint_clicked(self, widget):
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)
self.btn_playlistfolder.set_label(filename or "")
children = self.btn_playlistfolder.get_children()
if children:
label = children.pop()
label.set_alignment(0., .5)
break

fs.destroy()

0 comments on commit 99c46e8

Please sign in to comment.