2012-10-20 18:05:39 +00:00
# coding: utf-8
2014-03-02 17:31:32 +00:00
# This file is part of Supysonic.
#
# Supysonic is a Python implementation of the Subsonic server API.
# Copyright (C) 2013 Alban 'spl0k' Féron
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2013-10-15 11:14:20 +00:00
from flask import request , send_file , Response
2013-12-27 21:53:07 +00:00
import requests
2012-12-02 15:42:25 +00:00
import os . path
2013-06-07 13:31:30 +00:00
from PIL import Image
2013-10-15 11:14:20 +00:00
import subprocess
2013-12-09 18:06:14 +00:00
import codecs
2013-12-27 21:53:07 +00:00
from xml . etree import ElementTree
2012-11-23 16:13:25 +00:00
2013-10-15 11:14:20 +00:00
import config , scanner
2014-03-30 15:53:54 +00:00
from web import app , store
from db import Track , Album , Artist , Folder , User , ClientPrefs , now
2014-03-04 21:56:53 +00:00
from . import get_entity
2012-10-20 18:05:39 +00:00
2013-10-15 11:14:20 +00:00
def prepare_transcoding_cmdline ( base_cmdline , input_file , input_format , output_format , output_bitrate ) :
if not base_cmdline :
return None
ret = base_cmdline . split ( )
for i in xrange ( len ( ret ) ) :
ret [ i ] = ret [ i ] . replace ( ' %s rcpath ' , input_file ) . replace ( ' %s rcfmt ' , input_format ) . replace ( ' %o utfmt ' , output_format ) . replace ( ' %o utrate ' , str ( output_bitrate ) )
return ret
2012-11-22 13:51:43 +00:00
@app.route ( ' /rest/stream.view ' , methods = [ ' GET ' , ' POST ' ] )
2012-10-20 18:05:39 +00:00
def stream_media ( ) :
2012-12-02 15:42:25 +00:00
status , res = get_entity ( request , Track )
if not status :
return res
2012-10-20 18:05:39 +00:00
2013-12-07 17:46:30 +00:00
maxBitRate , format , timeOffset , size , estimateContentLength , client = map ( request . args . get , [ ' maxBitRate ' , ' format ' , ' timeOffset ' , ' size ' , ' estimateContentLength ' , ' c ' ] )
2013-10-15 08:32:35 +00:00
if format :
format = format . lower ( )
2012-10-20 18:05:39 +00:00
2013-10-15 11:14:20 +00:00
src_suffix = res . suffix ( )
dst_suffix = res . suffix ( )
dst_bitrate = res . bitrate
dst_mimetype = res . content_type
2013-12-07 17:46:30 +00:00
if client :
2014-03-30 15:53:54 +00:00
prefs = store . get ( ClientPrefs , ( request . user . id , client ) )
2013-12-07 17:46:30 +00:00
if not prefs :
prefs = ClientPrefs ( user_id = request . user . id , client_name = client )
2014-03-30 15:53:54 +00:00
store . add ( prefs )
2012-10-20 18:05:39 +00:00
2013-12-07 17:46:30 +00:00
if prefs . format :
dst_suffix = prefs . format
if prefs . bitrate and prefs . bitrate < dst_bitrate :
dst_bitrate = prefs . bitrate
2013-10-15 11:14:20 +00:00
2013-12-07 17:46:30 +00:00
if maxBitRate :
try :
maxBitRate = int ( maxBitRate )
except :
return request . error_formatter ( 0 , ' Invalid bitrate value ' )
if dst_bitrate > maxBitRate and maxBitRate != 0 :
dst_bitrate = maxBitRate
if format and format != ' raw ' and format != src_suffix :
dst_suffix = format
dst_mimetype = scanner . get_mime ( dst_suffix )
2013-10-15 11:14:20 +00:00
2013-12-07 17:46:30 +00:00
if format != ' raw ' and ( dst_suffix != src_suffix or dst_bitrate != res . bitrate ) :
2013-10-15 11:14:20 +00:00
transcoder = config . get ( ' transcoding ' , ' transcoder_ {} _ {} ' . format ( src_suffix , dst_suffix ) )
decoder = config . get ( ' transcoding ' , ' decoder_ ' + src_suffix ) or config . get ( ' transcoding ' , ' decoder ' )
encoder = config . get ( ' transcoding ' , ' encoder_ ' + dst_suffix ) or config . get ( ' transcoding ' , ' encoder ' )
if not transcoder and ( not decoder or not encoder ) :
transcoder = config . get ( ' transcoding ' , ' transcoder ' )
if not transcoder :
return request . error_formatter ( 0 , ' No way to transcode from {} to {} ' . format ( src_suffix , dst_suffix ) )
2013-10-14 16:36:45 +00:00
2013-10-15 11:14:20 +00:00
transcoder , decoder , encoder = map ( lambda x : prepare_transcoding_cmdline ( x , res . path , src_suffix , dst_suffix , dst_bitrate ) , [ transcoder , decoder , encoder ] )
2013-10-20 15:27:20 +00:00
try :
if transcoder :
proc = subprocess . Popen ( transcoder , stdout = subprocess . PIPE )
else :
dec_proc = subprocess . Popen ( decoder , stdout = subprocess . PIPE )
proc = subprocess . Popen ( encoder , stdin = dec_proc . stdout , stdout = subprocess . PIPE )
except :
return request . error_formatter ( 0 , ' Error while running the transcoding process ' )
2013-10-15 11:14:20 +00:00
def transcode ( ) :
while True :
data = proc . stdout . read ( 8192 )
if not data :
break
yield data
proc . terminate ( )
proc . wait ( )
2013-12-07 17:46:30 +00:00
app . logger . info ( ' Transcoding track {0.id} for user {1.id} . Source: {2} at {0.bitrate} kbps. Dest: {3} at {4} kbps ' . format ( res , request . user , src_suffix , dst_suffix , dst_bitrate ) )
2013-10-15 11:14:20 +00:00
response = Response ( transcode ( ) , mimetype = dst_mimetype )
else :
2013-10-15 16:26:41 +00:00
response = send_file ( res . path , mimetype = dst_mimetype )
2013-10-15 11:14:20 +00:00
2013-06-07 18:35:21 +00:00
res . play_count = res . play_count + 1
res . last_play = now ( )
2013-06-13 16:17:33 +00:00
request . user . last_play = res
request . user . last_play_date = now ( )
2014-03-30 15:53:54 +00:00
store . commit ( )
2013-06-07 18:35:21 +00:00
2013-10-15 11:14:20 +00:00
return response
2012-10-20 18:05:39 +00:00
2013-06-12 19:29:42 +00:00
@app.route ( ' /rest/download.view ' , methods = [ ' GET ' , ' POST ' ] )
def download_media ( ) :
status , res = get_entity ( request , Track )
if not status :
return res
return send_file ( res . path )
2012-11-22 13:51:43 +00:00
@app.route ( ' /rest/getCoverArt.view ' , methods = [ ' GET ' , ' POST ' ] )
2012-11-11 20:39:26 +00:00
def cover_art ( ) :
2012-12-02 15:42:25 +00:00
status , res = get_entity ( request , Folder )
if not status :
return res
if not res . has_cover_art or not os . path . isfile ( os . path . join ( res . path , ' cover.jpg ' ) ) :
2012-11-11 20:39:26 +00:00
return request . error_formatter ( 70 , ' Cover art not found ' )
size = request . args . get ( ' size ' )
if size :
try :
size = int ( size )
except :
return request . error_formatter ( 0 , ' Invalid size value ' )
else :
2012-12-02 15:42:25 +00:00
return send_file ( os . path . join ( res . path , ' cover.jpg ' ) )
2012-11-11 20:39:26 +00:00
2012-12-02 15:42:25 +00:00
im = Image . open ( os . path . join ( res . path , ' cover.jpg ' ) )
2012-11-11 20:39:26 +00:00
if size > im . size [ 0 ] and size > im . size [ 1 ] :
2012-12-02 15:42:25 +00:00
return send_file ( os . path . join ( res . path , ' cover.jpg ' ) )
2012-11-11 20:39:26 +00:00
2013-07-15 12:37:51 +00:00
size_path = os . path . join ( config . get ( ' base ' , ' cache_dir ' ) , str ( size ) )
2012-12-02 15:42:25 +00:00
path = os . path . join ( size_path , str ( res . id ) )
2012-11-11 20:39:26 +00:00
if os . path . exists ( path ) :
return send_file ( path )
if not os . path . exists ( size_path ) :
os . makedirs ( size_path )
im . thumbnail ( [ size , size ] , Image . ANTIALIAS )
im . save ( path , ' JPEG ' )
return send_file ( path )
2013-11-24 16:20:11 +00:00
@app.route ( ' /rest/getLyrics.view ' , methods = [ ' GET ' , ' POST ' ] )
def lyrics ( ) :
artist , title = map ( request . args . get , [ ' artist ' , ' title ' ] )
if not artist :
return request . error_formatter ( 10 , ' Missing artist parameter ' )
if not title :
return request . error_formatter ( 10 , ' Missing title parameter ' )
2014-03-30 15:53:54 +00:00
query = store . find ( Track , Album . id == Track . album_id , Artist . id == Album . artist_id , Track . title . like ( title ) , Artist . name . like ( artist ) )
2013-11-24 16:20:11 +00:00
for track in query :
lyrics_path = os . path . splitext ( track . path ) [ 0 ] + ' .txt '
if os . path . exists ( lyrics_path ) :
2013-12-09 18:06:14 +00:00
app . logger . debug ( ' Found lyrics file: ' + lyrics_path )
try :
lyrics = read_file_as_unicode ( lyrics_path )
except UnicodeError :
# Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or
# return no lyrics. Log it anyway.
app . logger . warn ( ' Unsupported encoding for lyrics file ' + lyrics_path )
continue
2013-11-24 16:20:11 +00:00
return request . formatter ( { ' lyrics ' : {
' artist ' : track . album . artist . name ,
' title ' : track . title ,
' _value_ ' : lyrics
} } )
2013-12-27 21:53:07 +00:00
try :
r = requests . get ( " http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect " ,
params = { ' artist ' : artist , ' song ' : title } )
root = ElementTree . fromstring ( r . content )
ns = { ' cl ' : ' http://api.chartlyrics.com/ ' }
return request . formatter ( { ' lyrics ' : {
' artist ' : root . find ( ' cl:LyricArtist ' , namespaces = ns ) . text ,
' title ' : root . find ( ' cl:LyricSong ' , namespaces = ns ) . text ,
' _value_ ' : root . find ( ' cl:Lyric ' , namespaces = ns ) . text
} } )
except requests . exceptions . RequestException , e :
app . logger . warn ( ' Error while requesting the ChartLyrics API: ' + str ( e ) )
2013-11-24 16:20:11 +00:00
return request . formatter ( { ' lyrics ' : { } } )
2013-12-09 18:06:14 +00:00
def read_file_as_unicode ( path ) :
""" Opens a file trying with different encodings and returns the contents as a unicode string """
encodings = [ ' utf-8 ' , ' latin1 ' ] # Should be extended to support more encodings
for enc in encodings :
try :
contents = codecs . open ( path , ' r ' , encoding = enc ) . read ( )
app . logger . debug ( ' Read file {} with {} encoding ' . format ( path , enc ) )
# Maybe save the encoding somewhere to prevent going through this loop each time for the same file
return contents
except UnicodeError :
pass
# Fallback to ASCII
app . logger . debug ( ' Reading file {} with ascii encoding ' . format ( path ) )
return unicode ( open ( path , ' r ' ) . read ( ) )