Added torrent support in new webplayer

This commit is contained in:
Fabio Manganiello 2019-07-01 19:32:22 +02:00
parent c78789e644
commit 277d6ec271
23 changed files with 695 additions and 210 deletions

View file

@ -46,11 +46,6 @@
@include appearance(none); @include appearance(none);
} }
&::-webkit-progress-value {
background: $slider-progress-bg;
height: 15px;
}
&::-moz-range-progress { &::-moz-range-progress {
background: $slider-progress-bg; background: $slider-progress-bg;
height: 15px; height: 15px;

View file

@ -10,6 +10,7 @@
@import 'webpanel/plugins/media/controls'; @import 'webpanel/plugins/media/controls';
@import 'webpanel/plugins/media/info'; @import 'webpanel/plugins/media/info';
@import 'webpanel/plugins/media/subs'; @import 'webpanel/plugins/media/subs';
@import 'webpanel/plugins/media/torrents';
.media-plugin { .media-plugin {
display: flex; display: flex;
@ -50,5 +51,10 @@
color: $result-item-icon; color: $result-item-icon;
margin-right: .5em; margin-right: .5em;
} }
.top-buttons {
text-align: right;
float: right;
}
} }

View file

@ -12,6 +12,9 @@
padding: .75em .5em; padding: .75em .5em;
border-bottom: $default-border-2; border-bottom: $default-border-2;
&:nth-child(odd) { background: rgba(255,255,255,0.0); }
&:nth-child(even) { background: $default-bg-3; }
&:hover { &:hover {
background: $hover-bg; background: $hover-bg;
border-radius: 1em; border-radius: 1em;

View file

@ -0,0 +1,40 @@
.media-plugin {
#media-torrents {
.modal {
width: 90%;
.body {
padding: 0;
text-align: center;
.head {
font-weight: bold;
padding: .5rem 0 2.5rem 0;
background: $torrents-head-bg;
border-bottom: $default-border-3;
}
.transfers-container {
margin: 0;
width: 100%;
}
.transfer {
display: flex;
padding: 1.5rem;
cursor: pointer;
&:nth-child(odd) { background: rgba(255,255,255,0.0); }
&:nth-child(even) { background: $default-bg-3; }
&.selected { background: $selected-bg; }
&:hover {
background: $hover-bg;
border-radius: .5rem;
}
}
}
}
}
}

View file

@ -17,3 +17,5 @@ $subs-control-height: 4rem;
$btn-default-shadow: 2px 2px 2px #ddd; $btn-default-shadow: 2px 2px 2px #ddd;
$btn-hover-default-shadow: 3px 3px 3px #ddd; $btn-hover-default-shadow: 3px 3px 3px #ddd;
$torrents-head-bg: #e8e8e8;

View file

@ -1,4 +1,4 @@
function execute(request) { function execute(request, timeout=30000) {
var additionalPayload = {}; var additionalPayload = {};
if (!('target' in request) || !request['target']) { if (!('target' in request) || !request['target']) {
@ -15,6 +15,10 @@ function execute(request) {
}; };
} }
if (timeout) {
additionalPayload.timeout = timeout;
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.post('/execute', request, additionalPayload) axios.post('/execute', request, additionalPayload)
.then((response) => { .then((response) => {
@ -42,7 +46,7 @@ function execute(request) {
}); });
} }
function request(action, args={}) { function request(action, args={}, timeout=30000) {
return execute({ return execute({
type: 'request', type: 'request',
action: action, action: action,

View file

@ -103,10 +103,10 @@ MediaHandlers.generic = MediaHandlers.file.extend({
}, },
methods: { methods: {
getMetadata: async function(url) { getMetadata: async function(item) {
return { return {
url: url, url: item.url,
title: url, title: item.url,
}; };
}, },
}, },

View file

@ -1,4 +1,4 @@
MediaHandlers.torrent = Vue.extend({ MediaHandlers.torrent = MediaHandlers.base.extend({
props: { props: {
bus: { type: Object }, bus: { type: Object },
iconClass: { iconClass: {
@ -31,6 +31,12 @@ MediaHandlers.torrent = Vue.extend({
}, },
}, },
data: function() {
return {
torrentStatus: {},
};
},
methods: { methods: {
matchesUrl: function(url) { matchesUrl: function(url) {
return !!( return !!(
@ -40,19 +46,207 @@ MediaHandlers.torrent = Vue.extend({
); );
}, },
getMetadata: function(url) { getMetadata: async function(item, onlyBase=false) {
// TODO let status = {};
return {};
if (!onlyBase)
status = await this.status({url: item.url});
let transferInfo = {};
if (item.url in this.torrentStatus)
transferInfo = this.torrentStatus[item.url];
return {...status, ...transferInfo};
}, },
play: function(item) { play: async function(item) {
let status = await this.download(item);
status.waitingPlay = true;
if (item.url in this.torrentStatus) {
this.firePlay(event);
}
}, },
download: function(item) { pause: async function(item) {
let status = await request('torrent.pause', {torrent: item.torrent});
this.mergeStatus(status);
}, },
info: function(item) { remove: async function(item) {
let status = await request('torrent.remove', {torrent: item.torrent});
this.mergeStatus(status);
}, },
status: async function(item) {
if (item) {
return await request('torrent.status', {
torrent: item.url,
});
}
return await request('torrent.status');
},
download: async function(item) {
let status = await this.status(item.url);
if (status && Object.keys(status).length) {
createNotification({
text: 'This torrent is already being downloaded, please play the downloading local media file instead',
image: {
icon: 'download',
},
});
return status;
}
status = await request(
'torrent.download',
{
torrent: item.url,
_async: true,
is_media: true,
},
timeout=120000 // Wait up to two minutes while downloading enough torrent chunks
);
this.torrentStatus[item.url] = {
...item, ...status,
scheduledPlay: false,
torrentState: status.state,
state: 'idle',
};
return this.torrentStatus[item.url];
},
onTorrentEvent: function(event) {
this.mergeStatus(event);
},
onTorrentQueued: function(event) {
if (!this.mergeStatus(event))
this.torrentStatus[event.url] = event;
createNotification({
text: 'Torrent download queued. Will start playing when enough chunks have been downloaded',
image: {
icon: 'clock',
},
});
},
onTorrentStart: function(event) {
if (!this.mergeStatus(event))
return;
createNotification({
text: 'Download of '.concat(event.name, ' started'),
image: {
icon: 'download',
},
});
},
onTorrentStop: function(event) {
if (!this.mergeStatus(event))
return;
delete this.torrentStatus[event.url];
this.bus.$emit('torrent-status-update', this.torrentStatus);
},
onTorrentProgress: function(event) {
if (!this.mergeStatus(event))
return;
if (this.torrentStatus[event.url].waitingPlay)
this.firePlay(event);
},
onTorrentCompleted: function(event) {
if (!this.mergeStatus(event))
return;
delete this.torrentStatus[event.url];
this.bus.$emit('torrent-status-update', this.torrentStatus);
createNotification({
text: 'Download of '.concat(event.name, ' completed'),
image: {
icon: 'check',
},
});
},
firePlay: function(item) {
if (!item.files || !item.files.length) {
console.warn('Torrent ' + item.url + ' has no media files available yet');
return;
}
if (event.progress < 5) {
console.warn('Please wait for enough chunks to be downloaded before playing');
return;
}
const url = 'file://' + item.files[0];
this.bus.$emit('play', {...item, type: 'file', url: url});
if (this.torrentStatus[item.url].waitingPlay)
this.torrentStatus[item.url].waitingPlay = false;
createNotification({
text: 'Playback of '.concat(item.name, ' started'),
image: {
icon: 'play',
},
});
},
mergeStatus: function(event) {
const torrentState = event.state;
delete event.state;
this.torrentStatus[event.url] = {
...this.torrentStatus[event.url],
...event,
torrentState: torrentState,
};
this.bus.$emit('torrent-status-update', this.torrentStatus);
return this.torrentStatus[event.url];
},
},
created: function() {
registerEventHandler(this.onTorrentStart, 'platypush.message.event.torrent.TorrentDownloadStartEvent');
registerEventHandler(this.onTorrentStop, 'platypush.message.event.torrent.TorrentDownloadStopEvent');
registerEventHandler(this.onTorrentProgress, 'platypush.message.event.torrent.TorrentDownloadProgressEvent');
registerEventHandler(this.onTorrentCompleted, 'platypush.message.event.torrent.TorrentDownloadCompletedEvent');
registerEventHandler(this.onTorrentQueued, 'platypush.message.event.torrent.TorrentQueuedEvent');
registerEventHandler(this.onTorrentEvent, 'platypush.message.event.torrent.TorrentPausedEvent',
'platypush.message.event.torrent.TorrentResumedEvent',
'platypush.message.event.torrent.TorrentDownloadStartEvent',
'platypush.message.event.torrent.TorrentStateChangeEvent');
const self = this;
this.status().then((status) => {
if (!status)
return;
for (const [url, torrent] of Object.entries(status)) {
self.torrentStatus[event.url]
self.mergeStatus(torrent);
}
});
setTimeout(() => {
self.bus.$on('torrent-play', self.firePlay);
self.bus.$on('torrent-pause', self.pause);
self.bus.$on('torrent-remove', self.remove);
}, 100);
}, },
}); });

View file

@ -41,7 +41,7 @@ MediaHandlers.youtube = MediaHandlers.base.extend({
return !!(url.match('^https?://(www\.)?youtube.com/') || url.match('^https?://youtu.be/') || url.match('^https?://.*googlevideo.com/')); return !!(url.match('^https?://(www\.)?youtube.com/') || url.match('^https?://youtu.be/') || url.match('^https?://.*googlevideo.com/'));
}, },
getMetadata: function(url) { getMetadata: function(item) {
return {}; return {};
}, },

View file

@ -61,6 +61,11 @@ Vue.component('media', {
item: {}, item: {},
}, },
torrentModal: {
visible: false,
items: {},
},
subsModal: { subsModal: {
visible: false, visible: false,
}, },
@ -71,6 +76,10 @@ Vue.component('media', {
types: function() { types: function() {
return MediaHandlers; return MediaHandlers;
}, },
torrentsDownloading: function() {
return Object.entries(this.torrentModal.items).length > 0;
},
}, },
methods: { methods: {
@ -154,10 +163,7 @@ Vue.component('media', {
}, },
info: function(item) { info: function(item) {
for (const [attr, value] of Object.entries(item)) { Vue.set(this.infoModal, 'item', item);
Vue.set(this.infoModal.item, attr, value);
}
this.infoModal.loading = false; this.infoModal.loading = false;
this.infoModal.visible = true; this.infoModal.visible = true;
}, },
@ -213,6 +219,14 @@ Vue.component('media', {
}; };
}, },
torrentStatusUpdate: function(torrents) {
Vue.set(this.torrentModal, 'items', {});
for (const [url, torrent] of Object.entries(torrents)) {
Vue.set(this.torrentModal.items, url, torrent);
}
},
onStatusUpdate: function(event) { onStatusUpdate: function(event) {
const dev = event.device; const dev = event.device;
const status = event.status; const status = event.status;
@ -307,6 +321,7 @@ Vue.component('media', {
this.bus.$on('status-update', this.onStatusUpdate); this.bus.$on('status-update', this.onStatusUpdate);
this.bus.$on('start-streaming', this.startStreaming); this.bus.$on('start-streaming', this.startStreaming);
this.bus.$on('search-subs', this.searchSubs); this.bus.$on('search-subs', this.searchSubs);
this.bus.$on('torrent-status-update', this.torrentStatusUpdate);
setInterval(this.timerFunc, 1000); setInterval(this.timerFunc, 1000);
}, },

View file

@ -0,0 +1,68 @@
Vue.component('media-torrents', {
template: '#tmpl-media-torrents',
mixins: [mediaUtils],
props: {
bus: { type: Object },
torrents: {
type: Object,
default: () => {},
},
},
data: function() {
return {
selectedItem: undefined,
};
},
computed: {
dropdownItems: function() {
const self = this;
return [
{
name: 'play',
text: 'Play',
iconClass: 'fa fa-play',
click: function() {
self.bus.$emit('torrent-play', self.selectedItem);
},
},
{
name: 'pause',
text: 'Pause/unpause transfer',
iconClass: 'fa fa-pause',
click: function() {
self.bus.$emit('torrent-pause', self.selectedItem);
},
},
{
name: 'cancel',
text: 'Cancel transfer',
iconClass: 'fa fa-trash',
click: function() {
self.bus.$emit('torrent-remove', self.selectedItem);
},
},
{
name: 'info',
text: 'View details',
iconClass: 'fa fa-info',
click: function() {
self.bus.$emit('info', self.selectedItem);
},
},
];
},
},
methods: {
openDropdown: function(item) {
this.selectedItem = item;
openDropdown(this.$refs.menu);
},
},
});

View file

@ -25,6 +25,8 @@
<img :src="image.src" v-if="image && image.src"> <img :src="image.src" v-if="image && image.src">
<i :class="['fa', 'fa-' + image.icon]" :style="image.color ? '--color: ' + image.color : ''" <i :class="['fa', 'fa-' + image.icon]" :style="image.color ? '--color: ' + image.color : ''"
v-else-if="image && image.icon"></i> v-else-if="image && image.icon"></i>
<i :class="image.iconClass" :style="image.color ? '--color: ' + image.color : ''"
v-else-if="image && image.iconClass"></i>
<i class="fa fa-exclamation" v-else-if="warning"></i> <i class="fa fa-exclamation" v-else-if="warning"></i>
<i class="fa fa-times" v-else-if="error"></i> <i class="fa fa-times" v-else-if="error"></i>
</div> </div>

View file

@ -4,6 +4,7 @@
{% include 'plugins/media/item.html' %} {% include 'plugins/media/item.html' %}
{% include 'plugins/media/info.html' %} {% include 'plugins/media/info.html' %}
{% include 'plugins/media/subs.html' %} {% include 'plugins/media/subs.html' %}
{% include 'plugins/media/torrents.html' %}
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/base.js') }}"></script> <script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/base.js') }}"></script>
@ -18,13 +19,20 @@
<script type="text/x-template" id="tmpl-media"> <script type="text/x-template" id="tmpl-media">
<div class="plugin media-plugin"> <div class="plugin media-plugin">
<div class="search"> <div class="search">
<div class="col-11"> <div class="col-10">
<media-search :bus="bus" <media-search :bus="bus"
:supportedTypes="types"> :supportedTypes="types">
</media-search> </media-search>
</div> </div>
<div class="col-1 pull-right"> <div class="col-2 top-buttons">
<button type="button"
v-if="torrentsDownloading"
@click="torrentModal.visible = !torrentModal.visible"
title="Show torrents progress">
<i class="fa fa-download"></i>
</button>
<media-devices :bus="bus" <media-devices :bus="bus"
:localPlayer="player"> :localPlayer="player">
</media-devices> </media-devices>
@ -55,6 +63,13 @@
ref="subs"> ref="subs">
</media-subs> </media-subs>
</modal> </modal>
<modal id="media-torrents" title="Torrents progress" v-model="torrentModal.visible">
<media-torrents :bus="bus"
:torrents="torrentModal.items"
ref="torrents">
</media-torrents>
</modal>
</div> </div>
</script> </script>

View file

@ -11,12 +11,17 @@
<div class="row" v-if="item.title"> <div class="row" v-if="item.title">
<div class="col-3 attr">Title</div> <div class="col-3 attr">Title</div>
<div class="col-9 value" v-text="item.title"></div> <div class="col-9 value">
<a :href="'https://imdb.com/title/' + item.imdb_id" v-if="item.imdb_id" target="_blank">
{% raw %}{{ item.title }}{% endraw %}
</a>
<span v-text="item.title" v-else></span>
</div>
</div> </div>
<div class="row" v-if="item.description"> <div class="row" v-if="item.description || item.synopsis">
<div class="col-3 attr">Description</div> <div class="col-3 attr">Description</div>
<div class="col-9 value" v-text="item.description"></div> <div class="col-9 value" v-text="item.description || item.synopsis"></div>
</div> </div>
<div class="row" v-if="item.channelTitle"> <div class="row" v-if="item.channelTitle">
@ -43,6 +48,16 @@
<div class="col-3 attr">Size</div> <div class="col-3 attr">Size</div>
<div class="col-9 value" v-text="convertSize(item.size)"></div> <div class="col-9 value" v-text="convertSize(item.size)"></div>
</div> </div>
<div class="row" v-if="item.peers != null || item.num_peers != null">
<div class="col-3 attr">Peers</div>
<div class="col-9 value" v-text="item.peers || item.num_peers"></div>
</div>
<div class="row" v-if="item.seeds != null">
<div class="col-3 attr">Seeds</div>
<div class="col-9 value" v-text="item.seeds"></div>
</div>
</div> </div>
</script> </script>

View file

@ -0,0 +1,31 @@
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/torrents.js') }}"></script>
<script type="text/x-template" id="tmpl-media-torrents">
<div class="transfers-container">
<div class="head">
<div class="col-4">Name</div>
<div class="col-2">State</div>
<div class="col-2">Progress</div>
<div class="col-2">Size</div>
<div class="col-2">DL rate</div>
</div>
<div class="transfers">
<div class="transfer"
:class="{selected: selectedItem && torrent.url === selectedItem.url}"
@click="openDropdown(torrent)"
v-for="torrent in torrents"
:key="torrent.url">
<div class="col-4 name" v-text="torrent.name || torrent.url"></div>
<div class="col-2 state" v-text="torrent.paused ? 'paused' : torrent.torrentState"></div>
<div class="col-2 progress">{% raw %}{{ torrent.progress + '%' }}{% endraw %}</div>
<div class="col-2 size" v-text="convertSize(torrent.size)"></div>
<div class="col-2 rate">{% raw %}{{ convertSize(torrent.download_rate) + '/s' }}{% endraw %}</div>
</div>
</div>
<dropdown ref="menu" :items="dropdownItems">
</dropdown>
</div>
</script>

View file

@ -5,7 +5,7 @@ import time
from queue import Queue from queue import Queue
from platypush.config import Config from platypush.config import Config
from platypush.message.event import Event, StopEvent from platypush.message.event import StopEvent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -13,8 +13,7 @@ logger = logging.getLogger(__name__)
class Bus(object): class Bus(object):
""" Main local bus where the daemon will listen for new messages """ """ Main local bus where the daemon will listen for new messages """
_MSG_EXPIRY_TIMEOUT = 60.0 # Consider a message on the bus as expired _MSG_EXPIRY_TIMEOUT = 60.0 # Consider a message on the bus as expired after one minute without being picked up
# after one minute without being picked up
def __init__(self, on_message=None): def __init__(self, on_message=None):
self.bus = Queue() self.bus = Queue()
@ -56,19 +55,19 @@ class Bus(object):
logger.warning('No message handlers installed, cannot poll') logger.warning('No message handlers installed, cannot poll')
return return
stop=False stop = False
while not stop: while not stop:
msg = self.get() msg = self.get()
if msg.timestamp and time.time() - msg.timestamp > self._MSG_EXPIRY_TIMEOUT: if msg.timestamp and time.time() - msg.timestamp > self._MSG_EXPIRY_TIMEOUT:
logger.debug('{} seconds old message on the bus expired, ignoring it: {}' logger.debug('{} seconds old message on the bus expired, ignoring it: {}'.
.format(int(time.time()-msg.timestamp), msg)) format(int(time.time()-msg.timestamp), msg))
continue continue
threading.Thread(target=self._msg_executor(msg)).start() threading.Thread(target=self._msg_executor(msg)).start()
if isinstance(msg, StopEvent) and msg.targets_me(): if isinstance(msg, StopEvent) and msg.targets_me():
logger.info('Received STOP event on the bus') logger.info('Received STOP event on the bus')
stop=True stop = True
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -5,72 +5,88 @@ class TorrentEvent(Event):
""" """
Base class for torrent events Base class for torrent events
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class TorrentDownloadingMetadataEvent(TorrentEvent): class TorrentQueuedEvent(TorrentEvent):
""" """
Event triggered upon torrent metadata download start Event triggered upon when a new torrent transfer is queued
""" """
def __init__(self, url, *args, **kwargs):
super().__init__(*args, url=url, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) class TorrentDownloadedMetadataEvent(TorrentEvent):
"""
Event triggered upon torrent metadata download completed
"""
def __init__(self, url, *args, **kwargs):
super().__init__(*args, url=url, **kwargs)
class TorrentDownloadStartEvent(TorrentEvent): class TorrentDownloadStartEvent(TorrentEvent):
""" """
Event triggered upon torrent download start Event triggered upon torrent download start
""" """
def __init__(self, url, *args, **kwargs):
def __init__(self, *args, **kwargs): super().__init__(*args, url=url, **kwargs)
super().__init__(*args, **kwargs)
class TorrentSeedingStartEvent(TorrentEvent): class TorrentSeedingStartEvent(TorrentEvent):
""" """
Event triggered upon torrent seeding start Event triggered upon torrent seeding start
""" """
def __init__(self, url, *args, **kwargs):
def __init__(self, *args, **kwargs): super().__init__(*args, url=url, **kwargs)
super().__init__(*args, **kwargs)
class TorrentDownloadProgressEvent(TorrentEvent): class TorrentDownloadProgressEvent(TorrentEvent):
""" """
Event triggered upon torrent download progress Event triggered upon torrent download progress
""" """
def __init__(self, url, *args, **kwargs):
def __init__(self, *args, **kwargs): super().__init__(*args, url=url, **kwargs)
super().__init__(*args, **kwargs)
class TorrentStateChangeEvent(TorrentEvent): class TorrentStateChangeEvent(TorrentEvent):
""" """
Event triggered upon torrent state change Event triggered upon torrent state change
""" """
def __init__(self, url, *args, **kwargs):
super().__init__(*args, url=url, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) class TorrentPausedEvent(TorrentEvent):
"""
Event triggered when a torrent transfer is paused
"""
def __init__(self, url, *args, **kwargs):
super().__init__(*args, url=url, **kwargs)
class TorrentResumedEvent(TorrentEvent):
"""
Event triggered when a torrent transfer is resumed
"""
def __init__(self, url, *args, **kwargs):
super().__init__(*args, url=url, **kwargs)
class TorrentDownloadCompletedEvent(TorrentEvent): class TorrentDownloadCompletedEvent(TorrentEvent):
""" """
Event triggered upon torrent state change Event triggered upon torrent state change
""" """
def __init__(self, url, *args, **kwargs):
def __init__(self, *args, **kwargs): super().__init__(*args, url=url, **kwargs)
super().__init__(*args, **kwargs)
class TorrentDownloadStopEvent(TorrentEvent): class TorrentDownloadStopEvent(TorrentEvent):
""" """
Event triggered when a torrent transfer is stopped Event triggered when a torrent transfer is stopped
""" """
def __init__(self, url, *args, **kwargs):
def __init__(self, *args, **kwargs): super().__init__(*args, url=url, **kwargs)
super().__init__(*args, **kwargs)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -30,8 +30,6 @@ class MediaPlugin(Plugin):
Requires: Requires:
* A media player installed (supported so far: mplayer, vlc, mpv, omxplayer, chromecast) * A media player installed (supported so far: mplayer, vlc, mpv, omxplayer, chromecast)
* The :class:`platypush.plugins.media.webtorrent` plugin for optional torrent support through webtorrent
(recommended)
* **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support over native library * **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support over native library
* **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support * **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support
* **requests** (``pip install requests``), optional, for local files over HTTP streaming supporting * **requests** (``pip install requests``), optional, for local files over HTTP streaming supporting
@ -138,9 +136,16 @@ class MediaPlugin(Plugin):
os.makedirs(self.download_dir, exist_ok=True) os.makedirs(self.download_dir, exist_ok=True)
self.media_dirs.add(self.download_dir) self.media_dirs.add(self.download_dir)
self._is_playing_torrent = False
self._videos_queue = [] self._videos_queue = []
@staticmethod
def _torrent_event_handler(evt_queue):
def handler(event):
# More than 5% of the torrent has been downloaded
if event.args.get('progress', 0) > 5 and event.args.get('files'):
evt_queue.put(event.args['files'])
return handler
def _get_resource(self, resource): def _get_resource(self, resource):
""" """
:param resource: Resource to play/parse. Supported types: :param resource: Resource to play/parse. Supported types:
@ -159,33 +164,28 @@ class MediaPlugin(Plugin):
resource = self.get_youtube_url(resource).output resource = self.get_youtube_url(resource).output
elif resource.startswith('magnet:?'): elif resource.startswith('magnet:?'):
try:
get_plugin('media.webtorrent')
return resource # media.webtorrent will handle this
except Exception:
pass
torrents = get_plugin('torrent')
self.logger.info('Downloading torrent {} to {}'.format( self.logger.info('Downloading torrent {} to {}'.format(
resource, self.download_dir)) resource, self.download_dir))
torrents = get_plugin('torrent')
evt_queue = queue.Queue()
torrents.download(resource, download_dir=self.download_dir, _async=True, is_media=True,
event_hndl=self._torrent_event_handler(evt_queue))
resources = [f for f in evt_queue.get()]
response = torrents.download(resource, download_dir=self.download_dir)
resources = [f for f in response.output if self._is_video_file(f)]
if resources: if resources:
self._videos_queue = sorted(resources) self._videos_queue = sorted(resources)
resource = self._videos_queue.pop(0) resource = self._videos_queue.pop(0)
else: else:
raise RuntimeError('Unable to download torrent {}'.format(resource)) raise RuntimeError('No media file found in torrent {}'.format(resource))
return resource return resource
def _stop_torrent(self): @staticmethod
if self._is_playing_torrent: def _stop_torrent():
try: torrents = get_plugin('torrent')
get_plugin('media.webtorrent').quit() torrents.quit()
except Exception as e:
self.logger.warning('Cannot quit the webtorrent instance: {}'.
format(str(e)))
@action @action
def play(self, resource, *args, **kwargs): def play(self, resource, *args, **kwargs):
@ -492,8 +492,7 @@ class MediaPlugin(Plugin):
[float(t) * pow(60, i) for (i, t) in enumerate(re.search( [float(t) * pow(60, i) for (i, t) in enumerate(re.search(
'^Duration:\s*([^,]+)', [x.decode() '^Duration:\s*([^,]+)', [x.decode()
for x in result.stdout.readlines() for x in result.stdout.readlines()
if "Duration" in x.decode()] if "Duration" in x.decode()].pop().strip()
.pop().strip()
).group(1).split(':')[::-1])] ).group(1).split(':')[::-1])]
) )

View file

@ -61,7 +61,6 @@ class MediaMplayerPlugin(MediaPlugin):
self._mplayer_timeout = mplayer_timeout self._mplayer_timeout = mplayer_timeout
self._mplayer_stopped_event = threading.Event() self._mplayer_stopped_event = threading.Event()
self._status_lock = threading.Lock() self._status_lock = threading.Lock()
self._is_playing_torrent = False
def _init_mplayer_bin(self, mplayer_bin=None): def _init_mplayer_bin(self, mplayer_bin=None):
if not mplayer_bin: if not mplayer_bin:
@ -259,11 +258,7 @@ class MediaMplayerPlugin(MediaPlugin):
resource = self._get_resource(resource) resource = self._get_resource(resource)
if resource.startswith('file://'): if resource.startswith('file://'):
resource = resource[7:] resource = resource[7:]
elif resource.startswith('magnet:?'):
self._is_playing_torrent = True
return get_plugin('media.webtorrent').play(resource)
self._is_playing_torrent = False
self._exec('loadfile', resource, mplayer_args=mplayer_args) self._exec('loadfile', resource, mplayer_args=mplayer_args)
self._post_event(MediaPlayEvent, resource=resource) self._post_event(MediaPlayEvent, resource=resource)
return self.status() return self.status()

View file

@ -2,7 +2,7 @@ import os
import re import re
import threading import threading
from platypush.context import get_bus, get_plugin from platypush.context import get_bus
from platypush.plugins.media import PlayerState, MediaPlugin from platypush.plugins.media import PlayerState, MediaPlugin
from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \ from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \
MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent, MediaSeekEvent MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent, MediaSeekEvent
@ -42,7 +42,6 @@ class MediaMpvPlugin(MediaPlugin):
self.args.update(args) self.args.update(args)
self._player = None self._player = None
self._is_playing_torrent = False
self._playback_rebounce_event = threading.Event() self._playback_rebounce_event = threading.Event()
self._on_stop_callbacks = [] self._on_stop_callbacks = []
@ -53,7 +52,7 @@ class MediaMpvPlugin(MediaPlugin):
if args: if args:
mpv_args.update(args) mpv_args.update(args)
for k,v in self._env.items(): for k, v in self._env.items():
os.environ[k] = v os.environ[k] = v
self._player = mpv.MPV(**mpv_args) self._player = mpv.MPV(**mpv_args)
@ -77,7 +76,8 @@ class MediaMpvPlugin(MediaPlugin):
if (evt == Event.FILE_LOADED or evt == Event.START_FILE) and self._get_current_resource(): if (evt == Event.FILE_LOADED or evt == Event.START_FILE) and self._get_current_resource():
self._playback_rebounce_event.set() self._playback_rebounce_event.set()
self._post_event(NewPlayingMediaEvent, resource=self._get_current_resource(), title=self._player.filename) self._post_event(NewPlayingMediaEvent, resource=self._get_current_resource(),
title=self._player.filename)
elif evt == Event.PLAYBACK_RESTART: elif evt == Event.PLAYBACK_RESTART:
self._playback_rebounce_event.set() self._playback_rebounce_event.set()
elif evt == Event.PAUSE: elif evt == Event.PAUSE:
@ -85,8 +85,8 @@ class MediaMpvPlugin(MediaPlugin):
elif evt == Event.UNPAUSE: elif evt == Event.UNPAUSE:
self._post_event(MediaPlayEvent, resource=self._get_current_resource(), title=self._player.filename) self._post_event(MediaPlayEvent, resource=self._get_current_resource(), title=self._player.filename)
elif evt == Event.SHUTDOWN or ( elif evt == Event.SHUTDOWN or (
evt == Event.END_FILE and event.get('event', {}).get('reason') evt == Event.END_FILE and event.get('event', {}).get('reason') in
in [EndFile.EOF_OR_INIT_FAILURE, EndFile.ABORTED, EndFile.QUIT]): [EndFile.EOF_OR_INIT_FAILURE, EndFile.ABORTED, EndFile.QUIT]):
playback_rebounced = self._playback_rebounce_event.wait(timeout=0.5) playback_rebounced = self._playback_rebounce_event.wait(timeout=0.5)
if playback_rebounced: if playback_rebounced:
self._playback_rebounce_event.clear() self._playback_rebounce_event.clear()
@ -95,8 +95,8 @@ class MediaMpvPlugin(MediaPlugin):
self._player = None self._player = None
self._post_event(MediaStopEvent) self._post_event(MediaStopEvent)
for callback in self._on_stop_callbacks: for cbk in self._on_stop_callbacks:
callback() cbk()
elif evt == Event.SEEK: elif evt == Event.SEEK:
self._post_event(MediaSeekEvent, position=self._player.playback_time) self._post_event(MediaSeekEvent, position=self._player.playback_time)
@ -111,7 +111,8 @@ class MediaMpvPlugin(MediaPlugin):
for regex in regexes: for regex in regexes:
m = re.search(regex, resource) m = re.search(regex, resource)
if m: return base_url + m.group(2) if m:
return base_url + m.group(2)
return None return None
@action @action
@ -149,14 +150,11 @@ class MediaMpvPlugin(MediaPlugin):
if resource.startswith('file://'): if resource.startswith('file://'):
resource = resource[7:] resource = resource[7:]
elif resource.startswith('magnet:?'):
self._is_playing_torrent = True
return get_plugin('media.webtorrent').play(resource)
else: else:
yt_resource = self._get_youtube_link(resource) yt_resource = self._get_youtube_link(resource)
if yt_resource: resource = yt_resource if yt_resource:
resource = yt_resource
self._is_playing_torrent = False
self._player.play(resource) self._player.play(resource)
return self.status() return self.status()
@ -283,7 +281,7 @@ class MediaMpvPlugin(MediaPlugin):
return self._player.sub_remove(sub_id) return self._player.sub_remove(sub_id)
@action @action
def toggle_fullscreen(self, fullscreen=None): def toggle_fullscreen(self):
""" Toggle the fullscreen mode """ """ Toggle the fullscreen mode """
return self.toggle_property('fullscreen') return self.toggle_property('fullscreen')
@ -296,14 +294,14 @@ class MediaMpvPlugin(MediaPlugin):
:param property: Property to toggle :param property: Property to toggle
""" """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
if not hasattr(self._player, property): if not hasattr(self._player, property):
self.logger.warning('No such mpv property: {}'.format(property)) self.logger.warning('No such mpv property: {}'.format(property))
value = not getattr(self._player, property) value = not getattr(self._player, property)
setattr(self._player, property, value) setattr(self._player, property, value)
return { property: value } return {property: value}
@action @action
def get_property(self, property): def get_property(self, property):
@ -312,7 +310,7 @@ class MediaMpvPlugin(MediaPlugin):
``man mpv`` for a full list of the available properties ``man mpv`` for a full list of the available properties
""" """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
return getattr(self._player, property) return getattr(self._player, property)
@action @action
@ -325,7 +323,7 @@ class MediaMpvPlugin(MediaPlugin):
:type props: dict :type props: dict
""" """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
for k,v in props.items(): for k,v in props.items():
setattr(self._player, k, v) setattr(self._player, k, v)
@ -340,7 +338,7 @@ class MediaMpvPlugin(MediaPlugin):
def remove_subtitles(self): def remove_subtitles(self):
""" Removes (hides) the subtitles """ """ Removes (hides) the subtitles """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
self._player.sub_visibility = False self._player.sub_visibility = False
@action @action
@ -368,7 +366,7 @@ class MediaMpvPlugin(MediaPlugin):
return (None, 'No mpv instance is running') return (None, 'No mpv instance is running')
mute = not self._player.mute mute = not self._player.mute
self._player.mute = mute self._player.mute = mute
return { 'muted': mute } return {'muted': mute}
@action @action
def set_position(self, position): def set_position(self, position):

View file

@ -159,9 +159,6 @@ class MediaVlcPlugin(MediaPlugin):
if resource.startswith('file://'): if resource.startswith('file://'):
resource = resource[len('file://'):] resource = resource[len('file://'):]
elif resource.startswith('magnet:?'):
self._is_playing_torrent = True
return get_plugin('media.webtorrent').play(resource)
self._init_vlc(resource) self._init_vlc(resource)
if subtitles: if subtitles:
@ -169,7 +166,6 @@ class MediaVlcPlugin(MediaPlugin):
subtitles = subtitles[len('file://'):] subtitles = subtitles[len('file://'):]
self._player.video_set_subtitle_file(subtitles) self._player.video_set_subtitle_file(subtitles)
self._is_playing_torrent = False
self._player.play() self._player.play()
if fullscreen or self._default_fullscreen: if fullscreen or self._default_fullscreen:

View file

@ -10,7 +10,7 @@ from platypush.config import Config
from platypush.context import get_bus, get_plugin from platypush.context import get_bus, get_plugin
from platypush.plugins.media import PlayerState, MediaPlugin from platypush.plugins.media import PlayerState, MediaPlugin
from platypush.message.event.torrent import TorrentDownloadStartEvent, \ from platypush.message.event.torrent import TorrentDownloadStartEvent, \
TorrentDownloadCompletedEvent, TorrentDownloadingMetadataEvent TorrentDownloadCompletedEvent, TorrentDownloadedMetadataEvent
from platypush.plugins import action from platypush.plugins import action
from platypush.utils import find_bins_in_path, find_files_by_ext, \ from platypush.utils import find_bins_in_path, find_files_by_ext, \
@ -142,7 +142,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
and state == TorrentState.IDLE: and state == TorrentState.IDLE:
# IDLE -> DOWNLOADING_METADATA # IDLE -> DOWNLOADING_METADATA
state = TorrentState.DOWNLOADING_METADATA state = TorrentState.DOWNLOADING_METADATA
bus.post(TorrentDownloadingMetadataEvent(resource=resource)) bus.post(TorrentDownloadedMetadataEvent(resource=resource))
elif 'downloading: ' in line.lower() \ elif 'downloading: ' in line.lower() \
and media_file is None: and media_file is None:
# Find video files in torrent directory # Find video files in torrent directory

View file

@ -10,9 +10,9 @@ import urllib.parse
from platypush.context import get_bus from platypush.context import get_bus
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
from platypush.message.event.torrent import \ from platypush.message.event.torrent import \
TorrentDownloadStartEvent, TorrentSeedingStartEvent, \ TorrentDownloadStartEvent, TorrentDownloadedMetadataEvent, TorrentStateChangeEvent, \
TorrentStateChangeEvent, TorrentDownloadProgressEvent, \ TorrentDownloadProgressEvent, TorrentDownloadCompletedEvent, TorrentDownloadStopEvent, \
TorrentDownloadCompletedEvent, TorrentDownloadStopEvent TorrentPausedEvent, TorrentResumedEvent, TorrentQueuedEvent
class TorrentPlugin(Plugin): class TorrentPlugin(Plugin):
@ -25,6 +25,9 @@ class TorrentPlugin(Plugin):
* **requests** (``pip install requests``) [optional] for torrent info URL download * **requests** (``pip install requests``) [optional] for torrent info URL download
""" """
# Wait time in seconds between two torrent transfer checks
_MONITOR_CHECK_INTERVAL = 3
default_torrent_ports = [6881, 6891] default_torrent_ports = [6881, 6891]
supported_categories = { supported_categories = {
'movies': 'https://movies-v2.api-fetch.website/movies/1', 'movies': 'https://movies-v2.api-fetch.website/movies/1',
@ -48,6 +51,7 @@ class TorrentPlugin(Plugin):
self.torrent_ports = torrent_ports if torrent_ports else self.default_torrent_ports self.torrent_ports = torrent_ports if torrent_ports else self.default_torrent_ports
self.download_dir = None self.download_dir = None
self._session = None
if download_dir: if download_dir:
self.download_dir = os.path.abspath(os.path.expanduser(download_dir)) self.download_dir = os.path.abspath(os.path.expanduser(download_dir))
@ -101,7 +105,6 @@ class TorrentPlugin(Plugin):
format(category, self.supported_categories.keys())) format(category, self.supported_categories.keys()))
self.logger.info('Searching {} torrents for "{}"'.format(category, query)) self.logger.info('Searching {} torrents for "{}"'.format(category, query))
url = 'https://{category}-v2.api-fetch.website/{category}/1'.format(category=category)
request = urllib.request.urlopen(urllib.request.Request( request = urllib.request.urlopen(urllib.request.Request(
self.supported_categories[category] + '?' + urllib.parse.urlencode({ self.supported_categories[category] + '?' + urllib.parse.urlencode({
'sort': 'relevance', 'sort': 'relevance',
@ -139,41 +142,18 @@ class TorrentPlugin(Plugin):
for (quality, item) in items.items() for (quality, item) in items.items()
], key=lambda _: _.get('seeds'), reverse=True) ], key=lambda _: _.get('seeds'), reverse=True)
@action def _get_torrent_info(self, torrent, download_dir):
def download(self, torrent, download_dir=None):
"""
Download a torrent.
:param torrent: Torrent to download. Supported formats:
* Magnet URLs
* Torrent URLs
* Local torrent file
:type resource: str
:param download_dir: Directory to download, overrides the default download_dir attribute (default: None)
:type download_dir: str
"""
import libtorrent as lt import libtorrent as lt
if not download_dir:
if self.download_dir:
download_dir = self.download_dir
else:
raise RuntimeError('download_dir not specified')
download_dir = os.path.abspath(os.path.expanduser(download_dir))
os.makedirs(download_dir, exist_ok=True)
info = {}
torrent_file = None torrent_file = None
magnet = None magnet = None
bus = get_bus() info = {}
file_info = {}
if torrent.startswith('magnet:?'): if torrent.startswith('magnet:?'):
magnet = torrent magnet = torrent
info = lt.parse_magnet_uri(magnet) info = lt.parse_magnet_uri(magnet)
info['magnet'] = magnet
elif torrent.startswith('http://') or torrent.startswith('https://'): elif torrent.startswith('http://') or torrent.startswith('https://'):
import requests import requests
response = requests.get(torrent, allow_redirects=True) response = requests.get(torrent, allow_redirects=True)
@ -196,11 +176,139 @@ class TorrentPlugin(Plugin):
'save_path': download_dir, 'save_path': download_dir,
} }
ses = lt.session() return info, file_info, torrent_file, magnet
ses.listen_on(*self.torrent_ports)
self.logger.info('Downloading "{}" to "{}" from [{}]' def _fire_event(self, event, event_hndl):
.format(info['name'], self.download_dir, torrent)) bus = get_bus()
bus.post(event)
try:
if event_hndl:
event_hndl(event)
except Exception as e:
self.logger.warning('Exception in torrent event handler: {}'.format(str(e)))
self.logger.exception(e)
def _torrent_monitor(self, torrent, transfer, download_dir, event_hndl, is_media):
def thread():
files = []
last_status = None
download_started = False
metadata_downloaded = False
while not transfer.is_finished():
if torrent not in self.transfers:
self.logger.info('Torrent {} has been stopped and removed'.format(torrent))
self._fire_event(TorrentDownloadStopEvent(url=torrent), event_hndl)
break
status = transfer.status()
torrent_file = transfer.torrent_file()
if torrent_file:
self.torrent_state[torrent]['size'] = torrent_file.total_size()
files = [os.path.join(
download_dir,
torrent_file.files().file_path(i))
for i in range(0, torrent_file.files().num_files())
]
if is_media:
from platypush.plugins.media import MediaPlugin
files = [f for f in files if MediaPlugin._is_video_file(f)]
self.torrent_state[torrent]['download_rate'] = status.download_rate
self.torrent_state[torrent]['name'] = status.name
self.torrent_state[torrent]['num_peers'] = status.num_peers
self.torrent_state[torrent]['paused'] = status.paused
self.torrent_state[torrent]['progress'] = round(100 * status.progress, 2)
self.torrent_state[torrent]['state'] = status.state.name
self.torrent_state[torrent]['title'] = status.name
self.torrent_state[torrent]['torrent'] = torrent
self.torrent_state[torrent]['upload_rate'] = status.upload_rate
self.torrent_state[torrent]['url'] = torrent
self.torrent_state[torrent]['files'] = files
if transfer.has_metadata() and not metadata_downloaded:
self._fire_event(TorrentDownloadedMetadataEvent(**self.torrent_state[torrent]), event_hndl)
metadata_downloaded = True
if status.state == status.downloading and not download_started:
self._fire_event(TorrentDownloadStartEvent(**self.torrent_state[torrent]), event_hndl)
download_started = True
if last_status and status.progress != last_status.progress:
self._fire_event(TorrentDownloadProgressEvent(**self.torrent_state[torrent]), event_hndl)
if not last_status or status.state != last_status.state:
self._fire_event(TorrentStateChangeEvent(**self.torrent_state[torrent]), event_hndl)
if last_status and status.paused != last_status.paused:
if status.paused:
self._fire_event(TorrentPausedEvent(**self.torrent_state[torrent]), event_hndl)
else:
self._fire_event(TorrentResumedEvent(**self.torrent_state[torrent]), event_hndl)
last_status = status
time.sleep(self._MONITOR_CHECK_INTERVAL)
if transfer and transfer.is_finished():
self._fire_event(TorrentDownloadCompletedEvent(**self.torrent_state[torrent]), event_hndl)
self.remove(torrent)
return files
return thread
@action
def download(self, torrent, download_dir=None, _async=False, event_hndl=None, is_media=False):
"""
Download a torrent.
:param torrent: Torrent to download. Supported formats:
* Magnet URLs
* Torrent URLs
* Local torrent file
:type torrent: str
:param download_dir: Directory to download, overrides the default download_dir attribute (default: None)
:type download_dir: str
:param _async: If true then the method will add the torrent to the transfer and then return. Updates on
the download status should be retrieved either by listening to torrent events or registering the
event handler. If false (default) then the method will exit only when the torrent download is complete.
:type _async: bool
:param event_hndl: A function that takes an event object as argument and is invoked upon a new torrent event
(download started, progressing, completed etc.)
:type event_hndl: function
:param is_media: Set it to true if you're downloading a media file that you'd like to stream as soon as the
first chunks are available. If so, then the events and the status method will only include media files
:type is_media: bool
"""
if torrent in self.torrent_state and torrent in self.transfers:
return self.torrent_state[torrent]
import libtorrent as lt
if not download_dir:
if self.download_dir:
download_dir = self.download_dir
else:
raise RuntimeError('download_dir not specified')
download_dir = os.path.abspath(os.path.expanduser(download_dir))
os.makedirs(download_dir, exist_ok=True)
info, file_info, torrent_file, magnet = self._get_torrent_info(torrent, download_dir)
if not self._session:
self._session = lt.session()
self._session.listen_on(*self.torrent_ports)
params = { params = {
'save_path': download_dir, 'save_path': download_dir,
@ -208,98 +316,64 @@ class TorrentPlugin(Plugin):
} }
if magnet: if magnet:
transfer = lt.add_magnet_uri(ses, magnet, params) transfer = lt.add_magnet_uri(self._session, magnet, params)
elif torrent_file: else:
params['ti'] = file_info params['ti'] = file_info
transfer = ses.add_torrent(params) transfer = self._session.add_torrent(params)
status = transfer.status()
files = []
self.transfers[torrent] = transfer self.transfers[torrent] = transfer
self.torrent_state[torrent] = { self.torrent_state[torrent] = {
'url': torrent, 'url': torrent,
'title': info['name'], 'title': transfer.status().name,
'trackers': info['trackers'], 'trackers': info['trackers'],
'save_path': download_dir, 'save_path': download_dir,
} }
bus.post(TorrentDownloadStartEvent(**self.torrent_state[torrent])) self._fire_event(TorrentQueuedEvent(url=torrent), event_hndl)
last_status = None self.logger.info('Downloading "{}" to "{}" from [{}]'.format(info['name'], download_dir, torrent))
monitor_thread = self._torrent_monitor(torrent=torrent, transfer=transfer, download_dir=download_dir,
event_hndl=event_hndl, is_media=is_media)
while not status.is_seeding: if not _async:
if not torrent in self.transfers: return monitor_thread()
self.logger.info('Torrent {} has been stopped and removed')
bus.post(TorrentDownloadStopEvent(url=torrent))
return
if not last_status:
bus.post(TorrentSeedingStartEvent(**self.torrent_state[torrent]))
status = transfer.status()
torrent_file = transfer.torrent_file()
if torrent_file:
files = [os.path.join(
self.download_dir,
torrent_file.files().file_path(i))
for i in range(0, torrent_file.files().num_files())
]
self.torrent_state[torrent]['progress'] = round(100 * status.progress, 2)
self.torrent_state[torrent]['download_rate'] = status.download_rate
self.torrent_state[torrent]['upload_rate'] = status.upload_rate
self.torrent_state[torrent]['num_peers'] = status.num_peers
self.torrent_state[torrent]['state'] = status.state
if last_status and status.progress != last_status.progress:
bus.post(TorrentDownloadProgressEvent(**self.torrent_state[torrent]))
if not last_status or status.state != last_status.state:
bus.post(TorrentStateChangeEvent(**self.torrent_state[torrent]))
self.logger.info(('Torrent download: {:.2f}% complete (down: {:.1f} kb/s ' +
'up: {:.1f} kB/s peers: {} state: {})')
.format(status.progress * 100,
status.download_rate / 1000,
status.upload_rate / 1000,
status.num_peers, status.state))
last_status = status
time.sleep(5)
if torrent_file:
try: os.unlink(torrent_file)
except: pass
bus.post(TorrentDownloadCompletedEvent(**self.torrent_state[torrent], files=files))
del self.torrent_state[torrent]
del self.transfers[torrent]
return files
threading.Thread(target=monitor_thread).start()
return self.torrent_state[torrent]
@action @action
def get_status(self): def status(self, torrent=None):
""" """
Get the status of the current transfers. Get the status of the current transfers.
:param torrent: Torrent path, URL or magnet URI whose status will be retrieve (default: None, retrieve all
current transfers)
:type torrent: str
:returns: A dictionary in the format torrent_url -> status :returns: A dictionary in the format torrent_url -> status
""" """
if torrent:
return self.torrent_state.get(torrent)
return self.torrent_state return self.torrent_state
@action @action
def pause(self, torrent): def pause(self, torrent):
""" """
Pause a torrent transfer. Pause/resume a torrent transfer.
:param torrent: Torrent URL as returned from `get_status()` :param torrent: Torrent URL as returned from `get_status()`
:type torrent: str :type torrent: str
""" """
if torrent not in self.transfers: if torrent not in self.transfers:
return (None, "No transfer in progress for {}".format(torrent)) return None, "No transfer in progress for {}".format(torrent)
self.transfers[torrent].pause() if self.torrent_state[torrent].get('paused', False):
self.transfers[torrent].resume()
else:
self.transfers[torrent].pause()
return self.torrent_state[torrent]
@action @action
def resume(self, torrent): def resume(self, torrent):
@ -311,7 +385,7 @@ class TorrentPlugin(Plugin):
""" """
if torrent not in self.transfers: if torrent not in self.transfers:
return (None, "No transfer in progress for {}".format(torrent)) return None, "No transfer in progress for {}".format(torrent)
self.transfers[torrent].resume() self.transfers[torrent].resume()
@ -325,17 +399,35 @@ class TorrentPlugin(Plugin):
""" """
if torrent not in self.transfers: if torrent not in self.transfers:
return (None, "No transfer in progress for {}".format(torrent)) return None, "No transfer in progress for {}".format(torrent)
self.transfers[torrent].pause() self.transfers[torrent].pause()
del self.torrent_state[torrent] del self.torrent_state[torrent]
del self.transfers[torrent] del self.transfers[torrent]
def _generate_rand_filename(self, length=16): if not len(self.transfers):
self.logger.info('No remaining active torrent transfers found, exiting session')
self._session = None
@action
def quit(self):
"""
Quits all the transfers and the active session
"""
if not self._session:
self.logger.info('No active sessions found')
return
transfers = self.transfers.copy()
for torrent in transfers:
self.remove(torrent)
@staticmethod
def _generate_rand_filename(length=16):
name = '' name = ''
for i in range(0, length): for i in range(0, length):
name += hex(random.randint(0, 15))[2:].upper() name += hex(random.randint(0, 15))[2:].upper()
return name + '.torrent' return name + '.torrent'
# vim:sw=4:ts=4:et:
# vim:sw=4:ts=4:et: