1 {% macro dirlisting(dir, path='') -%}
2 <ul class="list-group">
3 {% if dir and dir.get_subdirs().items() %}
4 {% for subdirname, subdirobj in dir.get_subdirs().items() %}
5 {% set subdirpath = os.path.relpath(subdirobj.fullpath, music_library.fullpath) %}
6 {% set subdirid = subdirpath.replace("/","-") %}
7 <li class="directory list-group-item list-group-item-primary">
8 <div class="btn-group" role="group">
9 <div class="btn-group" role="group">
10 <button type="button" class="btn btn-success btn-sm"
11 onclick="request('/post', {add_folder : '{{ subdirpath }}'})">
12 <i class="fa fa-plus" aria-hidden="true"></i>
14 <div class="btn-group" role="group">
15 <button id="btnGroupDrop2" type="button" class="btn btn-success btn-sm dropdown-toggle btn-space" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
16 <div class="dropdown-menu" aria-labelledby="btnGroupDrop2" style="">
17 <a class="dropdown-item"
18 onclick="request('/post', {add_folder : '{{ subdirpath }}'})">
19 <i class="fa fa-folder" aria-hidden="true"></i> Entire folder
21 <a class="dropdown-item"
22 onclick="request('/post', {add_folder_recursively : '{{ subdirpath }}'})">
23 <i class="fa fa-folder" aria-hidden="true"></i> Entire folder and sub-folders
31 <div class="btn-group lead"><div class="btn-space"><i class="fa fa-folder" aria-hidden="true"></i></div><a class="lead" data-toggle="collapse"
32 data-target="#multiCollapse-{{ subdirid }}" aria-expanded="true"
33 aria-controls="multiCollapse-{{ subdirid }}" href="#"> {{ subdirpath }}/</a>
36 <div class="btn-group" style="float: right;">
37 <form action="./download" method="get" class="directory">
38 <input type="text" value="{{ subdirpath }}" name="directory" hidden>
39 <button type="submit" class="btn btn-primary btn-sm btn-space"><i class="fa fa-download" aria-hidden="true"></i></button>
41 <button type="submit" class="btn btn-danger btn-sm btn-space"
42 onclick="request('/post', {delete_folder : '{{ subdirpath }}'}, true)">
43 <i class="fas fa-trash-alt"></i>
47 <div class="collapse multi-collapse" id="multiCollapse-{{ subdirid }}">
48 {{ dirlisting(subdirobj, subdirpath) -}}
52 {% set files = dir.get_files() %}
54 {% for file in files %}
55 {% set filepath = os.path.relpath(os.path.join(dir.fullpath, file), music_library.fullpath) %}
56 <li class="file list-group-item">
57 <div class="btn-group" role="group">
58 <div class="btn-group" role="group">
59 <button type="button" class="btn btn-success btn-sm"
60 onclick="request('/post', {add_file_bottom : '{{ filepath }}'})">
61 <i class="fa fa-plus" aria-hidden="true"></i>
63 <div class="btn-group" role="group">
64 <button id="btnGroupDrop2" type="button" class="btn btn-success btn-sm dropdown-toggle btn-space" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
65 <div class="dropdown-menu" aria-labelledby="btnGroupDrop2" style="">
66 <a class="dropdown-item"
67 onclick="request('/post', {add_file_bottom : '{{ filepath }}'})">
68 <i class="fa fa-angle-down" aria-hidden="true"></i> To bottom of play list
70 <a class="dropdown-item"
71 onclick="request('/post', {add_file_next : '{{ filepath }}'})">
72 <i class="fa fa-angle-right" aria-hidden="true"></i> After current song
79 <div class="btn-group lead">
80 <div class="btn-space"><i class="fa fa-music" aria-hidden="true"></i></div>
83 {% if tags_lookup[filepath] %}
84 {% for tag in tags_lookup[filepath] %}
85 <span class="badge badge-{{ tags_color_lookup[tag] }}">{{ tag }}</span>
89 <div class="btn-group" style="float: right;">
90 <form action="./download" method="get" class="file file_download">
91 <input type="text" value="{{ filepath }}" name="file" hidden>
92 <button type="submit" class="btn btn-primary btn-sm btn-space"><i class="fa fa-download" aria-hidden="true"></i></button>
94 <button type="submit" class="btn btn-danger btn-sm btn-space"
95 onclick="request('/post', {delete_music_file : '{{ filepath }}'}, true)">
96 <i class="fas fa-trash-alt"></i>
108 <meta charset="UTF-8">
109 <title>botamusique web interface</title>
110 <link rel="stylesheet" href="static/css/bootstrap.min.css">
111 <link rel="stylesheet" href="static/css/custom.css">
112 <META HTTP-EQUIV="Pragma" CONTENT="no-cache">
113 <META HTTP-EQUIV="Expires" CONTENT="-1">
117 <div class="container">
118 <div class="bs-docs-section">
119 <div class="page-header" id="banner">
120 <h1><i class="fa fa-music" aria-hidden="true"></i> botamusique Web Interface</h1>
123 <div class="bs-docs-section">
126 <div id="playlist" class="col-lg-12">
128 <div class="btn-group" style="margin-bottom: 10px;">
129 <button type="button" id="play-btn" class="btn btn-info btn-space"
130 onclick="request('post', {action : 'resume'})" disabled>
131 <i class="fas fa-play" aria-hidden="true"></i>
134 <button type="button" id="pause-btn" class="btn btn-warning btn-space"
135 onclick="request('post', {action : 'pause'})" disabled>
136 <i class="fas fa-pause" aria-hidden="true"></i>
139 <button type="button" id="stop-btn" class="btn btn-danger btn-space"
140 onclick="request('post', {action : 'stop'})" disabled>
141 <i class="fas fa-stop" aria-hidden="true"></i>
145 <div class="btn-group" style="float: right;">
146 <button type="button" id="oneshot-btn" class="btn btn-primary btn-space"
147 title="One-shot Mode"
148 onclick="request('post', {action : 'one-shot'})" disabled>
149 <i class="fas fa-tasks" aria-hidden="true"></i>
152 <button type="button" id="random-btn" class="btn btn-primary btn-space"
154 onclick="request('post', {action : 'randomize'})" disabled>
155 <i class="fas fa-random" aria-hidden="true"></i>
158 <button type="button" id="repeat-btn" class="btn btn-primary btn-space"
160 onclick="request('post', {action : 'repeat'})" disabled>
161 <i class="fas fa-redo" aria-hidden="true"></i>
164 <button type="button" id="autoplay-btn" class="btn btn-primary btn-space"
165 title="Autoplay Mode"
166 onclick="request('post', {action : 'autoplay'})" disabled>
167 <i class="fas fa-robot" aria-hidden="true"></i>
170 <button type="button" class="btn btn-warning btn-space"
171 onclick="request('post', {action : 'volume_down'})">
172 <i class="fa fa-volume-down" aria-hidden="true"></i>
174 <button type="button" class="btn btn-warning btn-space"
175 onclick="request('post', {action : 'volume_up'})">
176 <i class="fa fa-volume-up" aria-hidden="true"></i>
180 <table class="table">
183 <th scope="col">#</th>
184 <th scope="col" class="playlist-title-td">Title</th>
185 <th scope="col">Url/Path</th>
186 <th scope="col">Action</th>
189 <tbody id="playlist-table">
190 <tr class="table-dark">
191 <td colspan="4" class="text-muted" style="text-align:center;"> Fetching playlist .... </td>
196 <div class="btn-group">
197 <button type="button" class="btn btn-danger btn-space"
198 onclick="request('post', {action : 'clear'})">
199 <i class="fas fa-trash-alt" aria-hidden="true"></i> Clear Playlist
208 <div class="bs-docs-section">
211 <div class="page-header">
212 <h1 id="forms">Music Library</h1>
215 <div class="card-header">
216 <h4 class="card-title">Tags</h4>
218 <div class="card-body">
219 {% for tag in tags_color_lookup.keys() %}
220 <span class="tag-click badge badge-{{ tags_color_lookup[tag] }}"
221 onclick="request('post', {add_tag : '{{ tag }}'})">
229 <div class="bs-docs-section">
232 <div id="browser" class="card">
233 <div class="card-header">
234 <h4 class="card-title">Files</h4>
237 <div class="card-body">
238 <div class="btn-group" style="margin-bottom: 5px;" role="group">
239 <button type="submit" class="btn btn-secondary btn-space"
240 onclick="request('/post', {action : 'rescan'}); location.reload()">
241 <i class="fas fa-sync-alt" aria-hidden="true"></i> Rescan Files
243 <form action="./download" method="get" class="directory form1">
244 <input type="text" value="./" name="directory" hidden>
245 <button type="submit" class="btn btn-secondary btn-space"><i class="fa fa-download" aria-hidden="true"></i> Download All</button>
247 <form method="post" class="directory form3">
248 <input type="text" value="./" name="add_folder_recursively" hidden>
249 <button type="submit" class="btn btn-secondary btn-space"><i class="fa fa-plus" aria-hidden="true"></i> Add All</button>
253 {{ dirlisting(music_library) }}
261 <div id="upload" class="container">
262 <div class="bs-docs-section">
266 <div class="card-header">
267 <h5 class="card-title">Upload File</h5>
269 <div class="card-body">
270 <form action="./upload" method="post" enctype="multipart/form-data">
271 <div class="row" style="margin-bottom: 5px;">
272 <div id="uploadBox" class="col-lg-7 input-group">
273 <div id="uploadField" style="display: flex; width: 100%">
274 <div class="custom-file btn-space">
275 <input type="file" name="file[]" class="custom-file-input" id="uploadSelectFile"
276 aria-describedby="uploadSubmit" value="Browse Music file" multiple/>
277 <label class="custom-file-label" for="uploadSelectFile">Choose file</label>
281 <div class="col-lg-4 input-group-append">
282 <span class="input-group-text">Upload To</span>
283 <input class="form-control btn-space" list="targetdirs" id="targetdir" name="targetdir"
284 placeholder="uploads" />
285 <datalist id="targetdirs">
286 <option value="uploads">
287 {% for dir in music_library.get_subdirs_recursively() %}
288 <option value="{{ dir }}">
292 <button class="btn btn-primary btn-space" type="submit"
293 id="uploadSubmit" style="margin-left: -5px;">Upload!</button>
302 <div class="bs-docs-section" style="margin-bottom: 150px;">
306 <div class="card-header">
307 <h5 class="card-title">Add URL</h5>
309 <div class="card-body">
310 <label>Add Youtube/Soundcloud URL</label>
311 <div class="input-group">
312 <input class="form-control btn-space" type="text" id="add_url_input" placeholder="URL...">
313 <button type="submit" class="btn btn-primary"
314 onclick="var $i = $('#add_url_input')[0]; request('/post', {add_url : $i.value }); $i.value = ''; ">Add URL</button>
321 <div class="card-header">
322 <h5 class="card-title">Add Radio</h5>
324 <div class="card-body">
325 <label>Add Radio URL</label>
326 <div class="input-group">
327 <input class="form-control btn-space" type="text" id="add_radio_input" placeholder="Radio Address...">
328 <button type="submit" class="btn btn-primary"
329 onclick="var $i = $('#add_radio_input')[0]; request('/post', {add_radio : $i.value }); $i.value = '';">Add Radio</button>
338 <script src="static/js/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
339 <script src="static/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
340 <script src="static/js/fontawesome.all.js" crossorigin="anonymous"></script>
343 $('#uploadSelectFile').on('change', function () {
345 var fileName = $(this).val().replace('C:\\fakepath\\', " ");
346 //replace the "Choose a file" label
347 $(this).next('.custom-file-label').html(fileName);
349 $('a.a-submit, button.btn-submit').on('click', function (event) {
350 $(event.target).closest('form').submit();
353 var playlist_ver = 0;
355 function request(url, _data, refresh=false){
361 200 : function(data) {
362 if (data.ver !== playlist_ver) {
364 playlist_ver = data.ver;
366 updateControls(data.empty, data.play, data.mode);
375 function displayPlaylist(data){
376 // console.info(data);
377 $("#playlist-table tr").remove();
379 var items = data.items;
380 $.each(items, function(index, item){
381 $("#playlist-table").append(item);
386 function updatePlaylist(){
391 200 : displayPlaylist
396 function updateControls(empty, play, mode){
398 $("#play-btn").prop("disabled", true);
399 $("#pause-btn").prop("disabled", true);
400 $("#stop-btn").prop("disabled", true);
403 $("#play-btn").prop("disabled", true);
404 $("#pause-btn").prop("disabled", false);
405 $("#stop-btn").prop("disabled", false);
407 $("#play-btn").prop("disabled", false);
408 $("#pause-btn").prop("disabled", true);
409 $("#stop-btn").prop("disabled", true);
412 if(mode === "one-shot"){
413 $("#oneshot-btn").removeClass("btn-secondary").addClass("btn-primary").prop("disabled", true);
414 $("#repeat-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
415 $("#random-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
416 $("#autoplay-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
417 }else if(mode === "repeat"){
418 $("#oneshot-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
419 $("#repeat-btn").removeClass("btn-secondary").addClass("btn-primary").prop("disabled", true);
420 $("#random-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
421 $("#autoplay-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
422 }else if(mode === "random"){
423 $("#oneshot-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
424 $("#repeat-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
425 $("#random-btn").removeClass("btn-secondary").addClass("btn-primary").prop("disabled", false); // This is a feature.
426 $("#autoplay-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
427 }else if(mode === "autoplay"){
428 $("#oneshot-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
429 $("#repeat-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
430 $("#random-btn").removeClass("btn-primary").addClass("btn-secondary").prop("disabled", false);
431 $("#autoplay-btn").removeClass("btn-secondary").addClass("btn-primary").prop("disabled", true);
436 // Check the version of playlist to see if update is needed.
437 setInterval(function(){
442 200 : function(data){
443 if(data.ver !== playlist_ver){
445 playlist_ver = data.ver;
447 updateControls(data.empty, data.play, data.mode);
453 $(document).ready(updatePlaylist);