Added torrent support in new webplayer
This commit is contained in:
parent
c78789e644
commit
277d6ec271
23 changed files with 695 additions and 210 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 {};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
68
platypush/backend/http/static/js/plugins/media/torrents.js
Normal file
68
platypush/backend/http/static/js/plugins/media/torrents.js
Normal 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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
31
platypush/backend/http/templates/plugins/media/torrents.html
Normal file
31
platypush/backend/http/templates/plugins/media/torrents.html
Normal 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>
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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])]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue