Make your own music player with Bottle0.13 + jPlayer2.5!

Introduction

The lightweight framework Bottle seems to be popular, so I will make a cloud music player for practice. The finished product is listed in Bitbucket, so only the main points will be explained. The operation sample is available at here. By the way, I have confirmed the operation with Python 2.6.

What is a cloud music player?

It's convenient that once you buy it, you can download it from any device such as iTunes or Amazon Cloud Player and play it. However, as a CD purchase kitchen, I still want to play it with the sound source file at hand. Google Music can upload files by itself, but the Japanese music industry is inflexible Therefore, it will be difficult to operate a similar service in Japan for the time being. Therefore, I always wanted to create my own cloud music player, so I will take this opportunity to implement it.

Think about how it works

Another option is to build a streaming server using Subsonic or Icecast. While this is easy and many players can play it, it has the disadvantage of incurring a processing load because the sound source is encoded on the server side. (In fact, I used to use this method until now, but there is a problem with the song selection operation.) This time, we'll leave most of the playback to the jQuery plugin jPlayer, an HTML5-based media player. I don't know if the lightweight framework Bottle comes into play because the processing part of blunt Python is just to list playlists, but I think it's just right for studying.

Bottle basics

Bottle allows you to create web apps with a really simple syntax.

au.py


# -*- coding: utf-8 -*-
from bottle import route, run #It seems to be common to retrieve only the necessary methods (?)

@route('/hello') #Specify the target path to execute the following function
def hello(): #The function name can be anything
    return 'Hello World!' #Contents to be output to the browser

if __name__ == "__main__": #Note that if you do not block it, an error will occur when you execute it from Apache described later.
    run(host='0.0.0.0', port=8080, debug=True, reloader=True)

For the argument of run ()

Execute the above script from the command line.

python au.py

If you access http: // server address: 8080 / hello from your browser, you should see "Hello World!".

Use template

Now, let's implement the desired cloud music player from here. Bottle's template engine is Django-like, so experienced Django users should be happy to use it easily. Detailed specifications can be found in the Original Document.

au.py


from bottle import route, template, TEMPLATE_PATH
import os

ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) #Absolute path to the folder where this script is located
TEMPLATE_PATH.insert(0, ROOT_PATH+'/templates') #Specify the folder to store the template file

@route('/')
def index():
    return template('player.html') #You can also pass parameters following the template file

In the template, we will prepare jPlayer immediately. Since the appearance is secondary, CSS uses [Blue Monday] that comes with jPlayer, and the HTML part is an audio player with a playlist on this site. I was allowed to refer to! Also, the intersection of each template is grouped into a separate template file with ```% rebase ()`. Note that this has a different format than Django. Please note that the jplayer.m3u.js and init.js passed in the parameters are scripts that you will create later.

templates/player.html


% rebase('base.html', css=['jplayer.blue.monday.css'], js=['jquery.jplayer.min.js', 'jplayer.playlist.min.js', 'jplayer.m3u.js', 'init.js']) <!--Since parameters can be passed after the template file, specify the CSS and JS files to be additionally read.-->
<div id="jquery_jplayer_N" class="jp-jplayer"></div>
<div id="jp_container_N" class="jp-audio">
    <div class="jp-type-playlist">
        <div class="jp-gui jp-interface">
            <ul class="jp-controls">
                <li><a href="javascript:;" class="jp-previous" tabindex="1">previous</a></li>
                <li><a href="javascript:;" class="jp-play" tabindex="1">play</a></li>
                <li><a href="javascript:;" class="jp-pause" tabindex="1">pause</a></li>
                <li><a href="javascript:;" class="jp-next" tabindex="1">next</a></li>
                <li><a href="javascript:;" class="jp-stop" tabindex="1">stop</a></li>
                <li><a href="javascript:;" class="jp-mute" tabindex="1" title="mute">mute</a></li>
                <li><a href="javascript:;" class="jp-unmute" tabindex="1" title="unmute">unmute</a></li>
                <li><a href="javascript:;" class="jp-volume-max" tabindex="1" title="max volume">max volume</a></li>
            </ul>
            <div class="jp-progress">
                <div class="jp-seek-bar">
                    <div class="jp-play-bar"></div>
                </div>
            </div>
            <div class="jp-volume-bar">
                <div class="jp-volume-bar-value"></div>
            </div>
            <div class="jp-time-holder">
                <div class="jp-current-time"></div>
                <div class="jp-duration"></div>
            </div>
            <ul class="jp-toggles">
                <li><a href="javascript:;" class="jp-shuffle" tabindex="1" title="shuffle">shuffle</a></li>
                <li><a href="javascript:;" class="jp-shuffle-off" tabindex="1" title="shuffle off">shuffle off</a></li>
                <li><a href="javascript:;" class="jp-repeat" tabindex="1" title="repeat">repeat</a></li>
                <li><a href="javascript:;" class="jp-repeat-off" tabindex="1" title="repeat off">repeat off</a></li>
            </ul>
        </div>
        <div class="jp-playlist">
            <ul>
                <li></li>
            </ul>
        </div>
    </div>
</div>
<div id="m3u_list"></div>

Next, write the template file for the common part.

templates/base.html


<!DOCTYPE html>
<html>
<head>
    <title>AuPy - Croud Music Player</title>
    <script type="text/javascript" src="js/jquery.min.js"></script>
% for item in css: <!--Read the CSS file passed as a parameter-->
    <link rel="stylesheet" type="text/css" href="css/{{item}}"> 
% end
% for item in js: <!--Read the JS file also passed as a parameter-->
    <script type="text/javascript" src="js/{{item}}"></script>
% end
</head>
<body>
{{!base}} <!--Here is the contents of the calling template file-->
</body>
</html>

At this stage, I can't run jPlayer yet.

Create a jQuery plugin that reads m3u files

Implement a plugin that will set the jPlayer playlist when you pass the m3u / m3u8 file. It can be implemented as a jQuery plugin by writing it in the format below.

;(function($) {
    $.fn.loadm3u = function (){
        //Processing content
    }
})(jQuery);

This time, we will create a plug-in that assumes the following usage.

$().loadm3u(m3u file path,Server-side music folder path,Path to be replaced(option))


 The plug-in has a function to automatically replace the path specified in advance. As a result, the m3u / m3u8 files spit out by foobar2000 etc. can be used as they are in the cloud music player.


#### **`js/jplayer.m3u.js`**

;(function($) { $.fn.loadm3u = function (filepath, server_music_dir, local_music_dir){ $.get(filepath, function(data){ //Get the m3u file passed as an argument var data_array = data.split(/\r\n|\r|\n/); //Split with line breaks var playlists = []; for (var i = 0; i < data_array.length; i++){ //Process line by line if(data_array[i] != ""){ if (typeof local_music_dir != "undefined") { data_array[i] = data_array[i].replace(local_music_dir+"\", "") //Remove the path to be replaced } unix_dir_path = data_array[i].replace(/\/g, "/") //Correct backslash to slash title = unix_dir_path.split("/"); //Split with a slash playlists.push({"title":title[title.length-1], "free":true, "mp3":server_music_dir+"/"+unix_dir_path}); //Store the file name and file path of the sound source in the list } }

        $("#jquery_jplayer_N").jPlayer("destroy"); //Initialize jPlayer

        var Playlist = new jPlayerPlaylist( //Playlists and options to load into jPlayer
            {
                jPlayer: "#jquery_jplayer_N",
                cssSelectorAncestor: "#jp_container_N"
            }, playlists, {
                supplied: "mp3" //Supported audio file formats
            }
        );
    });
}

})(jQuery);


 In addition, although only mp3 is specified in the above script, it seems that it will be played without problems even if you pass a music file such as ogg just for the file path. flac didn't work because the browser doesn't support playback.

## Generate JS file for initialization in Python

 First, prepare a script to describe the settings collectively.


#### **`setting.py`**
```py

#! -*- coding: utf-8 -*-
import os

SERVER_MUSIC_ADDR = os.path.expandvars('audio') #Relative to music folders/Absolute URL
SERVER_PLAYLIST_ADDR = os.path.expandvars('playlist') #Relative to playlist folders/Absolute URL
SERVER_PLAYLIST_DIR = os.path.expandvars('/home/example/aupy/playlist') #Absolute path of playlist folder
LOCAL_MUSIC_DIR = os.path.expandvars('C:\Users\example\Music') #Absolute path of the local music folder to be replaced

Next, list the files in the playlist folder on the server, add them to `` <div id =" m3u_list "> </ div> `, and when the user clicks on the item, load it into the plugin you just created. Output the JS file to be set in Python.

au.py


from bottle import route, response
from setting import * #Read the configuration file

@route('/js/init.js') #Hijack the request for this JS file
def initjs():
    if LOCAL_MUSIC_DIR:
        local_music_dir = ', "'+LOCAL_MUSIC_DIR.replace('\\', '\\\\')+'"' #Set the replacement target path
    else:
        local_music_dir = ''
    
    output = '$(function(){\n' #Start writing output contents
    files = os.listdir(SERVER_PLAYLIST_DIR) #List files in playlist folder
    files.sort()
    flg = True
    for file in files:
        if file.startswith('.'): #.To skip htaccess etc.
            continue
        id = file.translate(None, '.')
        output += '$("#m3u_list").append("<a href=\\"#\\" id=\\"m3u_file_'+id+'\\" class=\\"list-group-item\\">'+file+'</a>");\n' #Add playlist file to HTML
        output += '$("#m3u_file_'+id+'").click(function(){ $().loadm3u("'+SERVER_PLAYLIST_ADDR+'/'+file+'", "'+SERVER_MUSIC_ADDR+'"'+local_music_dir+'); });\n' #Click trigger to pass m3u file to plugin
        if flg:
            output += '$().loadm3u("'+SERVER_PLAYLIST_ADDR+'/'+file+'", "'+SERVER_MUSIC_ADDR+'"'+local_music_dir+');\n' #Automatically load the first playlist
            flg = False
    output += '\n});'

    response.content_type = 'text/javascript' #Set MIME Type to JS
    return output

Am I the only one who can obfuscate the code at once by implementing the #string operation?

How to handle static files

Finally, upload the music file and playlist to the server and the player is complete. Requests for these static files can be accepted by Bottle as shown below, but it is more stable if they are properly accepted by Apache, which will be described later.

au.py


from bottle import route, static_file

@route('/css/<filename>')
def css(filename):
    return static_file(filename, root=ROOT_PATH+'/css')

@route('/js/<filename>')
def js(filename):
    return static_file(filename, root=ROOT_PATH+'/js')

@route('/audio/<filepath:path>')
def audio(filepath):
    return static_file(filepath, root=ROOT_PATH+'/audio')

@route('/playlist/<filename>')
def playlist(filename):
    return static_file(filename, root=ROOT_PATH+'/playlist'

Basic authentication

It is copyright-friendly to make music files accessible to anyone, so set a password. Basic authentication can also be applied to Bottle. This time, we will get the authentication information from `` `.htpasswd``` which is the password file for Apache Basic authentication. However, if you enable both Apache Basic authentication, it will not work properly, so be sure to disable either one.

au.py


from bottle import route, auth_basic
from crypt import crypt

def check(user, passwd): #Create your own password check function(In other words, it can also be used for colander authentication)
    for line in open(ROOT_PATH+'/.htpasswd', 'r'): #.Get line by line from htpasswd
        htpasswd = line[:-1].split(':') #Separate username and encrypted password
        if user is not None and htpasswd[1] is None: #.Enable authentication even if only the user name is set
            if user == htpasswd[0]:
                return True
        elif user is not None and passwd is not None:
            if user == htpasswd[0] and crypt(passwd, htpasswd[1][0:2]) == htpasswd[1]: #Match username and encrypted password
                return True
    return False

@route('/')
@auth_basic(check) #Add to the request requesting authentication
def index():
    return template('player.html')

Run from Apache + WSGI

The server function of Bottle is difficult to stabilize, so it is better to limit it to development only. This time, I will introduce an example of calling from Apache, but it depends greatly on the environment, so please use it as a reference. First, create an adapter in the WSGI file that will be called from Apache and passed to Bottle.

adapter.wsgi


import sys, os
sys.path.append(os.path.dirname(os.path.abspath(__file__))) #Pass the path of the folder where the script is located
import bottle, au #Load the Bottle itself and the app
application = bottle.default_app()

Next, write the following in a file suitable for your environment, such as Apache's httpd.conf or wsgi.conf, or other virtual host configuration file.

httpd.conf


WSGIScriptAlias /aupy /home/example/aupy/adapter.wsgi #Specify adapter
<Directory "/home/example/aupy/">
   Options ExecCGI Indexes FollowSymlinks
   AllowOverride All #.Enable htaccess
   Order deny,allow
   Allow from all
</Directory>

Alias /aupy/audio /home/example/aupy/audio
<Directory "/home/example/aupy/audio">
   AllowOverride All
   Order deny,allow
   Allow from all
</Directory>

Alias /aupy/playlist /home/example/aupy/playlist
<Directory "/home/example/aupy/playlist">
   AllowOverride All
   Order deny,allow
   Allow from all
</Directory>

Also, if you apply Basic authentication to the music folder, it seems that it will not start playing correctly on Android Chrome, so as a countermeasure for direct access, I compromised with restrictions by referrer or user agent (specify a specific model).

.htaccess


SetEnvIf Referer "^http://example.com.*" allowReferer #Allow access from cloud music player
SetEnvIf User-Agent "example" allowReferer #Allowed for certain UAs
order deny,allow
deny from all
allow from env=allowReferer

Supports Japanese file names

I forgot to write the important thing. Since I am a Western music HM kitchen, there is no problem, but I think the general public will want to play music with Japanese file names. Unfortunately I didn't know how to route multibyte URLs in Bottle so please let me know if you can. Apache can handle multi-byte URLs by introducing mod_encoding.

Introducing the commands executed in CentOS 6.2. Please note that the path etc. differs depending on the distribution. First, download the necessary files from the console.

wget http://webdav.todo.gr.jp/download/mod_encoding-20021209.tar.gz
wget http://iij.dl.sourceforge.jp/webdav/13905/mod_encoding.c.apache2.20040616
wget http://www.aconus.com/~oyaji/faq/mod_encoding.c-apache2.2-20060520.patch

Extract the downloaded file and apply the patch.

tar zxvf mod_encoding-20021209.tar.gz
cd mod_encoding-20021209/
./configure --with-apxs=/usr/sbin/apxs
cp ../mod_encoding.c.apache2.20040616 mod_encoding.c
patch -p0 < ../mod_encoding.c-apache2.2-20060520.patch

Install the included iconv_hook library.

cd lib
./configure
make
sudo make install
sudo vi /etc/ld.so.conf
/usr/local/Add lib
ldconfig

Finally, install mod_encoding.

cd ../
make
gcc -shared -o mod_encoding.so mod_encoding.o -Wc,-Wall -L/usr/local/lib -Llib -liconv_hook
sudo make install

Add the following to the Apache configuration file.

httpd.conf


<IfModule mod_encoding.c>
EncodingEngine On
SetServerEncoding UTF-8
DefaultClientEncoding UTF-8 CP932 EUCJP-MS

AddClientEncoding "RMA/*" CP932
AddClientEncoding "xdwin9x/" CP932
AddClientEncoding "cadaver/" UTF-8 EUCJP-MS
AddClientEncoding "Mozilla/" EUCJP-MS
</IfModule>

Finally, restart Apache with sudo service httpd restart and you should be able to access URLs containing multibytes correctly.

in conclusion

How was the introduction of Bottle? By all means, even geeks should try to make one cloud music player for each family!

Recommended Posts

Make your own music player with Bottle0.13 + jPlayer2.5!
Try to make your own AWS-SDK with bash
Make your own module quickly with setuptools (python)
[Python] Make your own LINE bot
Make your own manual. [Linux] [man]
Solve your own maze with Q-learning
Train UGATIT with your own dataset
Make your own VPC with a Single Public Subnet Only with boto
How to make your own domain site with heroku (free plan)
Make a video player with PySimpleGUI + OpenCV
[Reinforcement learning] DQN with your own library
Create your own DNS server with Twisted
Create your own Composite Value with SQLAlchemy
Make your own PC for deep learning
To import your own module with jupyter
Publish your own Python library with Homebrew
Argument implementation (with code) in your own language
Make your photos pictorial with Pillow's Mode Filter
Make your Python environment "easy" with VS Code
Steps to install your own library with pip
Flow of creating your own package with setup.py with python
Memo to create your own Box with Pepper's Python
Call your own C library with Go using cgo
Write your own activation function with Pytorch (hard sigmoid)
Let's call your own C ++ library with Python (Preferences)
[# 2] Make Minecraft with Python. ~ Model drawing and player implementation ~
Define your own distance function with k-means of scikit-learn
Make Jupyter Notebook your own: Change background and icons