feat: huge feature: a floating player, with a movable playhead
This commit is contained in:
parent
b050546e39
commit
0b7d0b8465
20
database.py
20
database.py
@ -194,7 +194,7 @@ class Condition:
|
|||||||
|
|
||||||
|
|
||||||
SETTING_DB_VERSION = 2
|
SETTING_DB_VERSION = 2
|
||||||
MUSIC_DB_VERSION = 2
|
MUSIC_DB_VERSION = 3
|
||||||
|
|
||||||
|
|
||||||
class SettingsDatabase:
|
class SettingsDatabase:
|
||||||
@ -503,7 +503,8 @@ class DatabaseMigration:
|
|||||||
self.settings_table_migrate_func = {0: self.settings_table_migrate_from_0_to_1,
|
self.settings_table_migrate_func = {0: self.settings_table_migrate_from_0_to_1,
|
||||||
1: self.settings_table_migrate_from_1_to_2}
|
1: self.settings_table_migrate_from_1_to_2}
|
||||||
self.music_table_migrate_func = {0: self.music_table_migrate_from_0_to_1,
|
self.music_table_migrate_func = {0: self.music_table_migrate_from_0_to_1,
|
||||||
1: self.music_table_migrate_from_1_to_2}
|
1: self.music_table_migrate_from_1_to_2,
|
||||||
|
2: self.music_table_migrate_from_2_to_3}
|
||||||
|
|
||||||
def migrate(self):
|
def migrate(self):
|
||||||
self.settings_database_migrate()
|
self.settings_database_migrate()
|
||||||
@ -564,7 +565,7 @@ class DatabaseMigration:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
log.info(f"database: no music table found. Creating music table version {MUSIC_DB_VERSION}.")
|
log.info(f"database: no music table found. Creating music table version {MUSIC_DB_VERSION}.")
|
||||||
self.create_music_table_version_2(conn)
|
self.create_music_table_version_3(conn)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@ -607,7 +608,7 @@ class DatabaseMigration:
|
|||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def create_music_table_version_2(self, conn):
|
def create_music_table_version_3(self, conn):
|
||||||
self.create_music_table_version_1(conn)
|
self.create_music_table_version_1(conn)
|
||||||
|
|
||||||
def settings_table_migrate_from_0_to_1(self, conn):
|
def settings_table_migrate_from_0_to_1(self, conn):
|
||||||
@ -669,3 +670,14 @@ class DatabaseMigration:
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
return 2 # return new version number
|
return 2 # return new version number
|
||||||
|
|
||||||
|
def music_table_migrate_from_2_to_3(self, conn):
|
||||||
|
items_to_update = self.music_db.query_music(Condition(), conn)
|
||||||
|
for item in items_to_update:
|
||||||
|
if 'duration' not in item:
|
||||||
|
item['duration'] = 0
|
||||||
|
|
||||||
|
self.music_db.insert_music(item)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return 3 # return new version number
|
||||||
|
17
interface.py
17
interface.py
@ -196,15 +196,19 @@ def playlist():
|
|||||||
title = item.format_title()
|
title = item.format_title()
|
||||||
artist = "??"
|
artist = "??"
|
||||||
path = ""
|
path = ""
|
||||||
|
duration = 0
|
||||||
if isinstance(item, FileItem):
|
if isinstance(item, FileItem):
|
||||||
path = item.path
|
path = item.path
|
||||||
if item.artist:
|
if item.artist:
|
||||||
artist = item.artist
|
artist = item.artist
|
||||||
|
duration = item.duration
|
||||||
elif isinstance(item, URLItem):
|
elif isinstance(item, URLItem):
|
||||||
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
||||||
|
duration = item.duration
|
||||||
elif isinstance(item, PlaylistURLItem):
|
elif isinstance(item, PlaylistURLItem):
|
||||||
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
||||||
artist = f" <a href=\"{item.playlist_url}\"><i>{item.playlist_title}</i></a>"
|
artist = f" <a href=\"{item.playlist_url}\"><i>{item.playlist_title}</i></a>"
|
||||||
|
duration = item.duration
|
||||||
elif isinstance(item, RadioItem):
|
elif isinstance(item, RadioItem):
|
||||||
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
path = f" <a href=\"{item.url}\"><i>{item.url}</i></a>"
|
||||||
|
|
||||||
@ -223,6 +227,7 @@ def playlist():
|
|||||||
'artist': artist,
|
'artist': artist,
|
||||||
'thumbnail': thumb,
|
'thumbnail': thumb,
|
||||||
'tags': tag_tuples,
|
'tags': tag_tuples,
|
||||||
|
'duration': duration
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -240,7 +245,9 @@ def status():
|
|||||||
'empty': False,
|
'empty': False,
|
||||||
'play': not var.bot.is_pause,
|
'play': not var.bot.is_pause,
|
||||||
'mode': var.playlist.mode,
|
'mode': var.playlist.mode,
|
||||||
'volume': var.bot.volume_set})
|
'volume': var.bot.volume_set,
|
||||||
|
'playhead': var.bot.playhead
|
||||||
|
})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return jsonify({'ver': var.playlist.version,
|
return jsonify({'ver': var.playlist.version,
|
||||||
@ -248,7 +255,9 @@ def status():
|
|||||||
'empty': True,
|
'empty': True,
|
||||||
'play': not var.bot.is_pause,
|
'play': not var.bot.is_pause,
|
||||||
'mode': var.playlist.mode,
|
'mode': var.playlist.mode,
|
||||||
'volume': var.bot.volume_set})
|
'volume': var.bot.volume_set,
|
||||||
|
'playhead': 0
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@web.route("/post", methods=['POST'])
|
@web.route("/post", methods=['POST'])
|
||||||
@ -335,6 +344,10 @@ def post():
|
|||||||
if len(var.playlist) >= int(request.form['play_music']):
|
if len(var.playlist) >= int(request.form['play_music']):
|
||||||
var.bot.play(int(request.form['play_music']))
|
var.bot.play(int(request.form['play_music']))
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
elif 'move_playhead' in request.form:
|
||||||
|
if float(request.form['move_playhead']) < var.playlist.current_item().item().duration:
|
||||||
|
log.info(f"web: move playhead to {float(request.form['move_playhead'])} s.")
|
||||||
|
var.bot.play(var.playlist.current_index, float(request.form['move_playhead']))
|
||||||
|
|
||||||
elif 'delete_item_from_library' in request.form:
|
elif 'delete_item_from_library' in request.form:
|
||||||
_id = request.form['delete_item_from_library']
|
_id = request.form['delete_item_from_library']
|
||||||
|
@ -6,6 +6,7 @@ import hashlib
|
|||||||
import mutagen
|
import mutagen
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
import util
|
||||||
import variables as var
|
import variables as var
|
||||||
from media.item import BaseItem, item_builders, item_loaders, item_id_generators, ValidationFailedError
|
from media.item import BaseItem, item_builders, item_loaders, item_id_generators, ValidationFailedError
|
||||||
import constants
|
import constants
|
||||||
@ -51,6 +52,7 @@ class FileItem(BaseItem):
|
|||||||
if os.path.exists(self.uri()):
|
if os.path.exists(self.uri()):
|
||||||
self._get_info_from_tag()
|
self._get_info_from_tag()
|
||||||
self.ready = "yes"
|
self.ready = "yes"
|
||||||
|
self.duration = util.get_media_duration(self.uri())
|
||||||
self.keywords = self.title + " " + self.artist
|
self.keywords = self.title + " " + self.artist
|
||||||
else:
|
else:
|
||||||
super().__init__(from_dict)
|
super().__init__(from_dict)
|
||||||
@ -75,7 +77,9 @@ class FileItem(BaseItem):
|
|||||||
"file: music file missed for %s" % self.format_debug_string())
|
"file: music file missed for %s" % self.format_debug_string())
|
||||||
raise ValidationFailedError(constants.strings('file_missed', file=self.path))
|
raise ValidationFailedError(constants.strings('file_missed', file=self.path))
|
||||||
|
|
||||||
# self.version += 1 # 0 -> 1, notify the wrapper to save me when validate() is visited the first time
|
if self.duration == 0:
|
||||||
|
self.duration = util.get_media_duration(self.uri())
|
||||||
|
self.version += 1 # 0 -> 1, notify the wrapper to save me
|
||||||
self.ready = "yes"
|
self.ready = "yes"
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -130,7 +134,8 @@ class FileItem(BaseItem):
|
|||||||
if not self.title:
|
if not self.title:
|
||||||
self.title = os.path.basename(file_no_ext)
|
self.title = os.path.basename(file_no_ext)
|
||||||
|
|
||||||
def _prepare_thumbnail(self, im):
|
@staticmethod
|
||||||
|
def _prepare_thumbnail(im):
|
||||||
im.thumbnail((100, 100), Image.ANTIALIAS)
|
im.thumbnail((100, 100), Image.ANTIALIAS)
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
im = im.convert('RGB')
|
im = im.convert('RGB')
|
||||||
|
@ -50,6 +50,7 @@ class BaseItem:
|
|||||||
self.path = ""
|
self.path = ""
|
||||||
self.tags = []
|
self.tags = []
|
||||||
self.keywords = ""
|
self.keywords = ""
|
||||||
|
self.duration = 0
|
||||||
self.version = 0 # if version increase, wrapper will re-save this item
|
self.version = 0 # if version increase, wrapper will re-save this item
|
||||||
|
|
||||||
if from_dict is None:
|
if from_dict is None:
|
||||||
@ -62,6 +63,7 @@ class BaseItem:
|
|||||||
self.title = from_dict['title']
|
self.title = from_dict['title']
|
||||||
self.path = from_dict['path']
|
self.path = from_dict['path']
|
||||||
self.keywords = from_dict['keywords']
|
self.keywords = from_dict['keywords']
|
||||||
|
self.duration = from_dict['duration']
|
||||||
|
|
||||||
def is_ready(self):
|
def is_ready(self):
|
||||||
return True if self.ready == "yes" else False
|
return True if self.ready == "yes" else False
|
||||||
@ -117,4 +119,5 @@ class BaseItem:
|
|||||||
"title": self.title,
|
"title": self.title,
|
||||||
"path": self.path,
|
"path": self.path,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
"keywords": self.keywords}
|
"keywords": self.keywords,
|
||||||
|
"duration": self.duration}
|
||||||
|
2
static/css/bootstrap.min.css
vendored
2
static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
@ -86,7 +86,7 @@
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme changer */
|
/* Theme changer and player button */
|
||||||
.floating-button {
|
.floating-button {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@ -100,7 +100,6 @@
|
|||||||
line-height: 52px;
|
line-height: 52px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 50px;
|
right: 50px;
|
||||||
bottom: 40px;
|
|
||||||
}
|
}
|
||||||
.floating-button:hover {
|
.floating-button:hover {
|
||||||
background-color: hsl(0, 0%, 43%);
|
background-color: hsl(0, 0%, 43%);
|
||||||
@ -122,7 +121,6 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: none;
|
display: none;
|
||||||
/* margin-bottom: 5px; */
|
|
||||||
}
|
}
|
||||||
#volume-popover[data-show] {
|
#volume-popover[data-show] {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -148,3 +146,69 @@
|
|||||||
#volume-popover[data-popper-placement^='top'] > #volume-popover-arrow {
|
#volume-popover[data-popper-placement^='top'] > #volume-popover-arrow {
|
||||||
bottom: -4px;
|
bottom: -4px;
|
||||||
}
|
}
|
||||||
|
#playerToast {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
#playerContainer {
|
||||||
|
display: flex;
|
||||||
|
height: 105px;
|
||||||
|
}
|
||||||
|
#playerArtwork {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
#playerArtworkIdle {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
#playerInfo {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 6px;
|
||||||
|
margin-left: 10px;
|
||||||
|
height: 80px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
#playerTitle {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#playerArtist {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
#playerActionBox {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
#playerBarBox {
|
||||||
|
margin-top: 5px;
|
||||||
|
height: 15px;
|
||||||
|
width: 400px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.scrolling {
|
||||||
|
animation: scrolling 8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes scrolling {
|
||||||
|
0% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
95%{
|
||||||
|
transform: translateX(-90%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -31,6 +31,8 @@ const playlist_table = $("#playlist-table");
|
|||||||
const playlist_empty = $("#playlist-empty");
|
const playlist_empty = $("#playlist-empty");
|
||||||
const playlist_expand = $(".playlist-expand");
|
const playlist_expand = $(".playlist-expand");
|
||||||
|
|
||||||
|
let playlist_items = null;
|
||||||
|
|
||||||
let playlist_ver = 0;
|
let playlist_ver = 0;
|
||||||
let playlist_current_index = 0;
|
let playlist_current_index = 0;
|
||||||
|
|
||||||
@ -69,6 +71,7 @@ function request(_url, _data, refresh = false) {
|
|||||||
checkForPlaylistUpdate();
|
checkForPlaylistUpdate();
|
||||||
}
|
}
|
||||||
updateControls(data.empty, data.play, data.mode, data.volume);
|
updateControls(data.empty, data.play, data.mode, data.volume);
|
||||||
|
updatePlayerPlayhead(data.playhead);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -119,6 +122,7 @@ function displayPlaylist(data) {
|
|||||||
playlist_loading.hide();
|
playlist_loading.hide();
|
||||||
$(".playlist-item").remove();
|
$(".playlist-item").remove();
|
||||||
let items = data.items;
|
let items = data.items;
|
||||||
|
playlist_items = items;
|
||||||
let length = data.length;
|
let length = data.length;
|
||||||
let start_from = data.start_from;
|
let start_from = data.start_from;
|
||||||
playlist_range_from = start_from;
|
playlist_range_from = start_from;
|
||||||
@ -149,6 +153,7 @@ function displayPlaylist(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
displayActiveItem(data.current_index);
|
displayActiveItem(data.current_index);
|
||||||
|
updatePlayerInfo(items[data.current_index]);
|
||||||
bindPlaylistEvent();
|
bindPlaylistEvent();
|
||||||
playlist_table.animate({ opacity: 1 }, 200);
|
playlist_table.animate({ opacity: 1 }, 200);
|
||||||
});
|
});
|
||||||
@ -221,17 +226,22 @@ function checkForPlaylistUpdate() {
|
|||||||
updatePlaylist();
|
updatePlaylist();
|
||||||
}
|
}
|
||||||
if (data.current_index !== playlist_current_index) {
|
if (data.current_index !== playlist_current_index) {
|
||||||
if ((data.current_index > playlist_range_to || data.current_index < playlist_range_from)
|
if (data.current_index !== -1) {
|
||||||
&& data.current_index !== -1) {
|
if ((data.current_index > playlist_range_to || data.current_index < playlist_range_from)) {
|
||||||
playlist_range_from = 0;
|
playlist_range_from = 0;
|
||||||
playlist_range_to = 0;
|
playlist_range_to = 0;
|
||||||
updatePlaylist();
|
updatePlaylist();
|
||||||
} else {
|
} else {
|
||||||
playlist_current_index = data.current_index;
|
playlist_current_index = data.current_index;
|
||||||
|
updatePlayerInfo(playlist_items[data.current_index]);
|
||||||
displayActiveItem(data.current_index);
|
displayActiveItem(data.current_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
updateControls(data.empty, data.play, data.mode, data.volume);
|
updateControls(data.empty, data.play, data.mode, data.volume);
|
||||||
|
if (!data.empty){
|
||||||
|
updatePlayerPlayhead(data.playhead);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -255,6 +265,7 @@ function bindPlaylistEvent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateControls(empty, play, mode, volume) {
|
function updateControls(empty, play, mode, volume) {
|
||||||
|
updatePlayerControls(play, empty);
|
||||||
if (empty) {
|
if (empty) {
|
||||||
playPauseBtn.prop('disabled', true);
|
playPauseBtn.prop('disabled', true);
|
||||||
fastForwardBtn.prop('disabled', true);
|
fastForwardBtn.prop('disabled', true);
|
||||||
@ -305,9 +316,6 @@ function changePlayMode(mode) {
|
|||||||
request('post', {action: mode});
|
request('post', {action: mode});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the version of playlist to see if update is needed.
|
|
||||||
setInterval(checkForPlaylistUpdate, 3000);
|
|
||||||
|
|
||||||
|
|
||||||
// ----------------------
|
// ----------------------
|
||||||
// --- THEME SWITCHER ---
|
// --- THEME SWITCHER ---
|
||||||
@ -337,7 +345,6 @@ function setPageTheme(theme) {
|
|||||||
document.getElementById("pagestyle").setAttribute("href", "static/css/bootstrap.darkly.min.css");
|
document.getElementById("pagestyle").setAttribute("href", "static/css/bootstrap.darkly.min.css");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------
|
// ---------------------
|
||||||
// ------ Browser ------
|
// ------ Browser ------
|
||||||
// ---------------------
|
// ---------------------
|
||||||
@ -844,12 +851,6 @@ function uploadStart(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setProgressBar(bar, progress) {
|
|
||||||
let prog_str = Math.floor(progress*100).toString();
|
|
||||||
bar.setAttribute("aria-valuenow", prog_str);
|
|
||||||
bar.style.width = prog_str + "%";
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUploadError(filename, error){
|
function setUploadError(filename, error){
|
||||||
let file_progress_item = filesProgressItem[filename];
|
let file_progress_item = filesProgressItem[filename];
|
||||||
|
|
||||||
@ -916,7 +917,8 @@ function uploadNextFile(){
|
|||||||
|
|
||||||
req.upload.addEventListener("progress", function(e){
|
req.upload.addEventListener("progress", function(e){
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
setProgressBar(file_progress_item.progress, e.loaded / e.total);
|
let percent = e.loaded / e.total;
|
||||||
|
setProgressBar(file_progress_item.progress, percent, Math.floor(percent) + "%");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -949,6 +951,153 @@ function uploadCancel(){
|
|||||||
areYouSureToCancelUploading = !areYouSureToCancelUploading;
|
areYouSureToCancelUploading = !areYouSureToCancelUploading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// ------ Player ------
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
const player = document.getElementById("playerToast");
|
||||||
|
const playerArtwork = document.getElementById("playerArtwork");
|
||||||
|
const playerArtworkIdle = document.getElementById("playerArtworkIdle");
|
||||||
|
const playerTitle = document.getElementById("playerTitle");
|
||||||
|
const playerArtist = document.getElementById("playerArtist");
|
||||||
|
const playerBar = document.getElementById("playerBar");
|
||||||
|
const playerBarBox = document.getElementById("playerBarBox");
|
||||||
|
const playerPlayBtn = document.getElementById("playerPlayBtn");
|
||||||
|
const playerPauseBtn = document.getElementById("playerPauseBtn");
|
||||||
|
const playerSkipBtn = document.getElementById("playerSkipBtn");
|
||||||
|
|
||||||
|
let currentPlayingItem = null;
|
||||||
|
|
||||||
|
function togglePlayer() {
|
||||||
|
$(player).toast("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerSetIdle(){
|
||||||
|
playerArtwork.style.display ='none';
|
||||||
|
playerArtworkIdle.style.display ='block';
|
||||||
|
playerTitle.textContent = '-- IDLE --';
|
||||||
|
playerArtist.textContent = '';
|
||||||
|
setProgressBar(playerBar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlayerInfo(item){
|
||||||
|
if (!item){
|
||||||
|
playerSetIdle();
|
||||||
|
}
|
||||||
|
playerArtwork.style.display ='block';
|
||||||
|
playerArtworkIdle.style.display ='none';
|
||||||
|
currentPlayingItem = item;
|
||||||
|
playerTitle.textContent = item.title;
|
||||||
|
playerArtist.textContent = item.artist;
|
||||||
|
playerArtwork.setAttribute("src", item.thumbnail);
|
||||||
|
|
||||||
|
if (isOverflown(playerTitle)) {
|
||||||
|
playerTitle.classList.add("scrolling")
|
||||||
|
} else {
|
||||||
|
playerTitle.classList.remove("scrolling")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOverflown(playerArtist)) {
|
||||||
|
playerArtist.classList.add("scrolling")
|
||||||
|
} else {
|
||||||
|
playerArtist.classList.remove("scrolling")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlayerControls(play, empty) {
|
||||||
|
if (empty) {
|
||||||
|
playerSetIdle();
|
||||||
|
playerPlayBtn.setAttribute("disabled", "");
|
||||||
|
playerPauseBtn.setAttribute("disabled", "");
|
||||||
|
playerSkipBtn.setAttribute("disabled", "");
|
||||||
|
} else {
|
||||||
|
playerPlayBtn.removeAttribute("disabled");
|
||||||
|
playerPauseBtn.removeAttribute("disabled");
|
||||||
|
playerSkipBtn.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
if (play) {
|
||||||
|
playerPlayBtn.style.display ='none';
|
||||||
|
playerPauseBtn.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
playerPlayBtn.style.display = 'block';
|
||||||
|
playerPauseBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let playhead_timer;
|
||||||
|
let player_playhead_position;
|
||||||
|
let playhead_dragging = false;
|
||||||
|
function updatePlayerPlayhead(playhead){
|
||||||
|
if (!currentPlayingItem || playhead_dragging){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentPlayingItem.duration !== 0 || currentPlayingItem.duration < playhead){
|
||||||
|
playerBar.classList.remove("progress-bar-animated");
|
||||||
|
clearInterval(playhead_timer);
|
||||||
|
player_playhead_position = playhead;
|
||||||
|
setProgressBar(playerBar, player_playhead_position / currentPlayingItem.duration, secondsToStr(player_playhead_position));
|
||||||
|
if (playing) {
|
||||||
|
playhead_timer = setInterval(function () {
|
||||||
|
player_playhead_position += 0.1;
|
||||||
|
setProgressBar(playerBar, player_playhead_position / currentPlayingItem.duration, secondsToStr(player_playhead_position));
|
||||||
|
}, 100); // delay in milliseconds
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (playing) {
|
||||||
|
playerBar.classList.add("progress-bar-animated");
|
||||||
|
} else {
|
||||||
|
playerBar.classList.remove("progress-bar-animated");
|
||||||
|
}
|
||||||
|
setProgressBar(playerBar, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerBarBox.addEventListener('mousedown', function () {
|
||||||
|
if (currentPlayingItem && currentPlayingItem.duration > 0){
|
||||||
|
playerBarBox.addEventListener('mousemove', playheadDragged);
|
||||||
|
clearInterval(playhead_timer);
|
||||||
|
playhead_dragging = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
playerBarBox.addEventListener('mouseup', function (event) {
|
||||||
|
playerBarBox.removeEventListener('mousemove', playheadDragged);
|
||||||
|
let percent = event.offsetX / playerBarBox.clientWidth;
|
||||||
|
request('post', {move_playhead: percent * currentPlayingItem.duration});
|
||||||
|
playhead_dragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function playheadDragged(event){
|
||||||
|
let percent = event.offsetX / playerBarBox.clientWidth;
|
||||||
|
setProgressBar(playerBar, percent, secondsToStr(percent * currentPlayingItem.duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// ------- Util -------
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
function isOverflown(element) {
|
||||||
|
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProgressBar(bar, progress, text="") {
|
||||||
|
let prog_str = (progress*100).toString();
|
||||||
|
bar.setAttribute("aria-valuenow", prog_str);
|
||||||
|
bar.style.width = prog_str + "%";
|
||||||
|
bar.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function secondsToStr(seconds) {
|
||||||
|
seconds = Math.floor(seconds);
|
||||||
|
let mins = Math.floor(seconds / 60);
|
||||||
|
let secs = seconds % 60;
|
||||||
|
return ("00" + mins).slice(-2) + ":" + ("00" + secs).slice(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
themeInit();
|
themeInit();
|
||||||
updateResults();
|
updateResults();
|
||||||
$(document).ready(updatePlaylist);
|
$(document).ready(updatePlaylist);
|
||||||
|
|
||||||
|
// Check the version of playlist to see if update is needed.
|
||||||
|
setInterval(checkForPlaylistUpdate, 3000);
|
||||||
|
@ -410,10 +410,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="floating-button" onclick="switchTheme()">
|
|
||||||
|
<div class="floating-button" style="bottom: 120px;" onclick="togglePlayer()">
|
||||||
|
<i class="fas fa-play" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="floating-button" style="bottom: 50px;" onclick="switchTheme()">
|
||||||
<i class="fas fa-lightbulb" aria-hidden="true"></i>
|
<i class="fas fa-lightbulb" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="playerToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
|
||||||
|
<div class="toast-header">
|
||||||
|
<i class="fas fa-play-circle mr-2 text-primary"></i>
|
||||||
|
<strong class="mr-auto">Now Playing...</strong>
|
||||||
|
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body" id="playerContainer">
|
||||||
|
<img id="playerArtworkIdle" src="static/image/empty_box.svg" />
|
||||||
|
<img id="playerArtwork" src="static/image/unknown-album.png" style="display: none;"/>
|
||||||
|
<div id="playerInfo">
|
||||||
|
<div id="playerActionBox">
|
||||||
|
<button id="playerPlayBtn" class="btn btn-primary btn-sm btn-space" style="display: none"
|
||||||
|
onclick="request('post', {action: 'resume'})">
|
||||||
|
<i class="fas fa-play"></i>
|
||||||
|
</button>
|
||||||
|
<button id="playerPauseBtn" class="btn btn-primary btn-sm btn-space" style="display: none"
|
||||||
|
onclick="request('post', {action: 'pause'})">
|
||||||
|
<i class="fas fa-pause"></i>
|
||||||
|
</button>
|
||||||
|
<button id="playerSkipBtn" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-fast-forward"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="overflow: hidden; max-width: 320px;">
|
||||||
|
<strong id="playerTitle">Song Title</strong>
|
||||||
|
</div>
|
||||||
|
<span id="playerArtist">Artist</span>
|
||||||
|
<div id="playerBarBox" class="progress">
|
||||||
|
<div id="playerBar" class="progress-bar" role="progressbar" aria-valuenow="50"
|
||||||
|
aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="footer" style="height:50px; width: 100%; margin-top: 100px;"></div>
|
||||||
|
|
||||||
<form id="download-form" action="download" method="GET" target="_blank">
|
<form id="download-form" action="download" method="GET" target="_blank">
|
||||||
<input hidden type="text" name="id" value="">
|
<input hidden type="text" name="id" value="">
|
||||||
<input hidden type="text" name="type" value="">
|
<input hidden type="text" name="type" value="">
|
||||||
|
17
util.py
17
util.py
@ -337,6 +337,22 @@ def youtube_search(query):
|
|||||||
log.error("util: youtube query failed with error:\n %s" % error_traceback)
|
log.error("util: youtube query failed with error:\n %s" % error_traceback)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_media_duration(path):
|
||||||
|
command = ("ffprobe", "-v", "quiet", "-show_entries", "format=duration",
|
||||||
|
"-of", "default=noprint_wrappers=1:nokey=1", path)
|
||||||
|
process = sp.Popen(command, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
|
stdout, stderr = process.communicate()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not stderr:
|
||||||
|
return float(stdout)
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class LoggerIOWrapper(io.TextIOWrapper):
|
class LoggerIOWrapper(io.TextIOWrapper):
|
||||||
def __init__(self, logger: logging.Logger, logging_level, fallback_io_buffer):
|
def __init__(self, logger: logging.Logger, logging_level, fallback_io_buffer):
|
||||||
super().__init__(fallback_io_buffer, write_through=True)
|
super().__init__(fallback_io_buffer, write_through=True)
|
||||||
@ -351,4 +367,3 @@ class LoggerIOWrapper(io.TextIOWrapper):
|
|||||||
else:
|
else:
|
||||||
self.logger.log(self.logging_level, text.rstrip())
|
self.logger.log(self.logging_level, text.rstrip())
|
||||||
super().write(text + "\n")
|
super().write(text + "\n")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user