├── LICENSE ├── README.md ├── dist ├── listen1_mac.dmg └── listen1_windows.zip ├── listen1 ├── __init__.py ├── app.py ├── environment.py ├── fabfile.py ├── handlers │ ├── __init__.py │ ├── base.py │ ├── douban.py │ ├── home.py │ ├── player.py │ ├── playlist.py │ ├── search.py │ └── trackfile.py ├── listen1.spec ├── logconfig │ ├── __init__.py │ ├── dictconfig.py │ └── logconfig.py ├── make.sh ├── media │ ├── css │ │ ├── angular-ui-notification.css │ │ ├── bootstrap.min.css │ │ ├── cover.css │ │ ├── player.css │ │ └── reset.css │ ├── images │ │ ├── favicon.ico │ │ ├── loading.gif │ │ ├── logo.png │ │ ├── mycover.jpg │ │ ├── placeholder.png │ │ ├── playbar.png │ │ ├── player_directplay.png │ │ ├── player_large.png │ │ ├── player_small.png │ │ ├── progress_indicator.png │ │ └── statbar.png │ └── js │ │ ├── app.js │ │ └── vendor │ │ ├── angular-soundmanager2.js │ │ ├── angular-ui-notification.js │ │ ├── angular.js │ │ └── jquery-1.12.2.js ├── models │ ├── __init__.py │ └── playlist.py ├── replay │ ├── __init__.py │ ├── douban.py │ ├── netease.py │ ├── qq.py │ ├── replay.py │ └── xiami.py ├── requirements │ ├── common.txt │ ├── dev.txt │ └── production.txt ├── res │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── settings.py ├── templates │ ├── base.html │ └── index.html ├── tests │ ├── __init__.py │ └── run_tests.py ├── trayshell │ ├── __init__.py │ ├── launcher.py │ ├── shell_base.py │ ├── shell_pyside.py │ └── utils.py └── urls.py ├── package_resource ├── mac │ └── dmg-resource │ │ ├── listen1.json │ │ ├── listen1_install_background.png │ │ ├── listen1_install_disk_icon.png │ │ └── listen1_install_disk_icon.png.icns └── windows │ └── listen1.wxs ├── readme_mac.md └── readme_windows.md /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Listen 1 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Listen 1 2 | ========== 3 | 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.txt) 5 | [![platform](https://img.shields.io/badge/python-2.7-green.svg)]() 6 | 7 | Listen 1 让你用一个网页就能听到多个网站的在线音乐(现已包括网易云音乐,QQ音乐,虾米音乐,豆瓣音乐)。你可以非常的简单的访问和收听在线音乐,而不用受到单个音乐网站资源不全的限制了。 8 | 9 | 它不仅能搜索多家在线音乐提供商的资源,还能方便的整理你喜欢的音乐,制作自己的歌单。尽兴的享受音乐吧! 10 | 11 | 支持浏览器:IE 11, Chrome, FireFox, Safari 12 | 13 | * 主页:[https://github.com/listen1/listen1](https://github.com/listen1/listen1) 14 | * 联系邮箱:githublisten1@gmail.com 15 | 16 | [![platform](http://i.imgur.com/if4CNr2.png?1)]() 17 | 18 | 19 | 安装 20 | ---- 21 | #### Windows 环境 22 | Windows安装包(点击下载):[listen1_windows.zip](https://raw.githubusercontent.com/listen1/listen1/master/dist/listen1_windows.zip) 23 | 24 | 1. 解压缩后,运行msi文件完成安装。 25 | 2. 点击桌面的Listen 1图标就会打开Listen 1的应用网页了。 26 | 27 | 注意:可能误触发杀毒软件的警报,请忽略。 28 | 29 | #### Mac 环境 30 | Mac安装包(点击下载):[listen1_mac.dmg](https://raw.githubusercontent.com/listen1/listen1/master/dist/listen1_mac.dmg) 31 | 32 | 1. 运行dmg文件完成安装。 33 | 2. 点击Listen 1图标就会打开Listen 1的应用网页了。 34 | 35 | 调试开发 36 | ---------- 37 | 后台基于tornado开发,可以用Python环境直接运行。 38 | 39 | 1. pip环境下安装在requirements下的package 40 | 41 | pip install -r requirements/dev.txt 42 | 43 | 2. python app.py 44 | 3. 访问 http://localhost:8888/ 45 | 46 | 打包 47 | ---- 48 | 打包注意事项请参考[readme_mac.md](https://github.com/listen1/listen1/blob/master/readme_mac.md)和[readme_windows.md](https://github.com/listen1/listen1/blob/master/readme_windows.md)。 49 | 50 | 致谢 51 | ---- 52 | 在开发过程中,参考了很多音乐网站API的分析代码和文章,感谢这些开发者的努力。(具体项目网址参考源码) 53 | 54 | 55 | License 56 | -------- 57 | MIT 58 | -------------------------------------------------------------------------------- /dist/listen1_mac.dmg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/dist/listen1_mac.dmg -------------------------------------------------------------------------------- /dist/listen1_windows.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/dist/listen1_windows.zip -------------------------------------------------------------------------------- /listen1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/__init__.py -------------------------------------------------------------------------------- /listen1/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import tornado.httpserver 4 | import tornado.ioloop 5 | import tornado.web 6 | from tornado.options import options 7 | 8 | from settings import settings 9 | from urls import url_patterns 10 | 11 | class TornadoBoilerplate(tornado.web.Application): 12 | def __init__(self): 13 | tornado.web.Application.__init__(self, url_patterns, **settings) 14 | 15 | 16 | def main(): 17 | app = TornadoBoilerplate() 18 | http_server = tornado.httpserver.HTTPServer(app) 19 | http_server.listen(options.port) 20 | tornado.ioloop.IOLoop.instance().start() 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /listen1/environment.py: -------------------------------------------------------------------------------- 1 | """Add the boilerplate's directories to Python's site-packages path. 2 | """ 3 | import os 4 | import site 5 | import sys 6 | 7 | ROOT = os.path.dirname(os.path.abspath(__file__)) 8 | path = lambda *a: os.path.join(ROOT, *a) 9 | 10 | prev_sys_path = list(sys.path) 11 | ''' 12 | site.addsitedir(path('handlers')) 13 | if os.path.exists(path('vendor')): 14 | for directory in os.listdir(path('vendor')): 15 | full_path = path('vendor/%s' % directory) 16 | if os.path.isdir(full_path): 17 | site.addsitedir(full_path) 18 | 19 | # Move the new items to the front of sys.path. (via virtualenv) 20 | new_sys_path = [] 21 | for item in list(sys.path): 22 | if item not in prev_sys_path: 23 | new_sys_path.append(item) 24 | sys.path.remove(item) 25 | sys.path[:0] = new_sys_path 26 | ''' -------------------------------------------------------------------------------- /listen1/fabfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Fabfile using only commands from buedafab (https://github.com/bueda/ops) to 3 | deploy this app to remote servers. 4 | """ 5 | 6 | import os 7 | from fabric.api import * 8 | 9 | from buedafab.test import (test, tornado_test_runner as _tornado_test_runner, 10 | lint) 11 | from buedafab.deploy.types import tornado_deploy as deploy 12 | from buedafab.environments import development, staging, production, localhost 13 | from buedafab.tasks import (setup, restart_webserver, rollback, enable, 14 | disable, maintenancemode, rechef) 15 | 16 | # For a description of these attributes, see https://github.com/bueda/ops 17 | 18 | env.unit = "boilerplate" 19 | env.path = "/var/webapps/%(unit)s" % env 20 | env.scm = "git@github.com:bueda/%(unit)s.git" % env 21 | env.scm_http_url = "http://github.com/bueda/%(unit)s" % env 22 | env.root_dir = os.path.abspath(os.path.dirname(__file__)) 23 | env.test_runner = _tornado_test_runner 24 | 25 | env.pip_requirements = ["requirements/common.txt", 26 | "vendor/allo/pip-requirements.txt",] 27 | env.pip_requirements_dev = ["requirements/dev.txt",] 28 | env.pip_requirements_production = ["requirements/production.txt",] 29 | -------------------------------------------------------------------------------- /listen1/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/handlers/__init__.py -------------------------------------------------------------------------------- /listen1/handlers/base.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import json 3 | import tornado.web 4 | 5 | import logging 6 | 7 | logger = logging.getLogger('listenone.' + __name__) 8 | 9 | 10 | class BaseHandler(tornado.web.RequestHandler): 11 | """A class to collect common handler methods - all other handlers should 12 | subclass this one. 13 | """ 14 | 15 | def load_json(self): 16 | """Load JSON from the request body and store them in 17 | self.request.arguments, like Tornado does by default for POSTed form 18 | parameters. 19 | 20 | If JSON cannot be decoded, raises an HTTPError with status 400. 21 | """ 22 | try: 23 | self.request.arguments = json.loads(self.request.body) 24 | except ValueError: 25 | msg = "Could not decode JSON: %s" % self.request.body 26 | logger.debug(msg) 27 | raise tornado.web.HTTPError(400, msg) 28 | 29 | def get_json_argument(self, name, default=None): 30 | """Find and return the argument with key 'name' from JSON request data. 31 | Similar to Tornado's get_argument() method. 32 | """ 33 | if default is None: 34 | default = self._ARG_DEFAULT 35 | if not self.request.arguments: 36 | self.load_json() 37 | if name not in self.request.arguments: 38 | if default is self._ARG_DEFAULT: 39 | msg = "Missing argument '%s'" % name 40 | logger.debug(msg) 41 | raise tornado.web.HTTPError(400, msg) 42 | logger.debug( 43 | "Returning default argument %s, as we couldn't find " 44 | "'%s' in %s" % (default, name, self.request.arguments)) 45 | return default 46 | arg = self.request.arguments[name] 47 | logger.debug("Found '%s': %s in JSON arguments" % (name, arg)) 48 | return arg 49 | -------------------------------------------------------------------------------- /listen1/handlers/douban.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import glob 3 | import json 4 | import logging 5 | import os 6 | import os.path 7 | import urllib 8 | import uuid 9 | 10 | import tornado.httpclient 11 | 12 | from handlers.base import BaseHandler 13 | from settings import MEDIA_ROOT 14 | from replay.douban import get_captcha_token, \ 15 | login, remove_douban_token_ck, get_douban_token_ck, set_douban_token_ck 16 | from models.playlist import PlaylistManager 17 | 18 | 19 | logger = logging.getLogger('listenone.' + __name__) 20 | 21 | 22 | class FetchManager(): 23 | def __init__(self, token=None, ck=None): 24 | self.status = 'stop' 25 | self.sid_list_count = 0 26 | self.sid_list = [] 27 | self.result = [] 28 | 29 | self.client = tornado.httpclient.AsyncHTTPClient() 30 | cookie = r'flag="ok"; ac="1458457738"; bid="2dLYThADnhQ"; _pk_ref' + \ 31 | '.100002.6447=%5B%22%22%2C%22%22%2C1458457740%2C%22https' + \ 32 | '%3A%2F%2Fmusic.douban.com%2F%22%5D; _pk_id.100002.6447=390c6c' + \ 33 | '20836ad808.1456577744.3.1458457764.1458264315.; dbcl2="' + \ 34 | token + '"; fmNlogin="y"; ck="' + ck + \ 35 | '"; openExpPan=Y; _ga=GA1.2.1945208436.1456577744' 36 | 37 | user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) ' + \ 38 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 ' + \ 39 | 'Safari/537.36' 40 | referer = 'http://douban.fm/' 41 | xwith = 'ShockwaveFlash/19.0.0.245' 42 | self.headers = { 43 | 'User-Agent': user_agent, 44 | 'Cookie': cookie, 45 | 'Referer': referer, 46 | 'X-Requested-With': xwith, 47 | 'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2', 48 | 'Accept-Encoding': 'gzip, deflate, sdch', 49 | } 50 | self.step = 20 51 | 52 | self.token = token 53 | self.ck = ck 54 | 55 | def start(self): 56 | if self.status == 'finished': 57 | return 58 | self.status = 'progress' 59 | 60 | url = 'http://douban.fm/j/v2/redheart/basic' 61 | request = tornado.httpclient.HTTPRequest(url=url, headers=self.headers) 62 | self.client.fetch(request, self._on_download) 63 | 64 | def _on_download(self, response): 65 | # get red heart sids 66 | result = json.loads(response.body) 67 | self.sid_list = [i['sid'] for i in result['songs']] 68 | self.sid_list_count = len(self.sid_list) 69 | self._on_download2() 70 | 71 | def _on_download2(self, response=None): 72 | if response is not None: 73 | for d in json.loads(response.body): 74 | real_url = d['url'] 75 | url = '/track_file?' + urllib.urlencode(dict( 76 | url=real_url, 77 | artist=d['singers'][0]['name'].encode("utf8"), 78 | album=d['albumtitle'].encode("utf8"), 79 | title=d['title'].encode("utf8"), 80 | id=d['sid'], 81 | source='douban')) 82 | track = { 83 | 'id': 'dbtrack_' + str(d['sid']), 84 | 'title': d['title'], 85 | 'artist': d['singers'][0]['name'], 86 | 'artist_id': 'dbartist_' + d['singers'][0]['id'], 87 | 'album': d['albumtitle'], 88 | 'album_id': 'dbalbum_' + d['aid'], 89 | 'img_url': d['picture'], 90 | 'url': url, 91 | 'source': 'douban', 92 | 'source_url': 'https://music.douban.com/subject/%s/' % 93 | d['aid'], 94 | } 95 | self.result.append(track) 96 | 97 | url = 'http://douban.fm/j/v2/redheart/songs' 98 | handle_list = self.sid_list[:self.step] 99 | 100 | if handle_list == []: 101 | self.status = 'finished' 102 | # create playlist for douban red heart songs 103 | PlaylistManager.shared_instance()\ 104 | .create_playlist(u'豆瓣红心', tracks=self.result) 105 | return 106 | 107 | self.sid_list = self.sid_list[self.step:] 108 | sids = '|'.join(handle_list) 109 | v = dict(sids=sids, kbps="192", ck=self.ck) 110 | request = tornado.httpclient.HTTPRequest( 111 | url=url, method='POST', 112 | body=urllib.urlencode(v), headers=self.headers) 113 | self.client.fetch(request, self._on_download2) 114 | logger.debug('fetch url for douban red heart:' + url) 115 | 116 | def get_status(self): 117 | # status: stop, progress, finish 118 | if self.sid_list_count == 0: 119 | progress = 0 120 | else: 121 | finished = self.sid_list_count - len(self.sid_list) 122 | progress = int(finished * 100 / self.sid_list_count) 123 | return dict(status=self.status, progress=progress, result=self.result) 124 | 125 | manager = None 126 | 127 | 128 | def _get_captcha(): 129 | # get valid code from douban server 130 | # save it to temp folder 131 | filename = str(uuid.uuid4()) + '.jpg' 132 | path = MEDIA_ROOT + '/temp/' + filename 133 | token = get_captcha_token(path) 134 | return dict(path='/static/temp/' + filename, token=token) 135 | 136 | 137 | def _clear_temp_folder(): 138 | path = MEDIA_ROOT + '/temp/' 139 | files = glob.glob(path + '*') 140 | for f in files: 141 | os.remove(f) 142 | 143 | 144 | class ValidCodeHandler(BaseHandler): 145 | def get(self): 146 | token, ck = get_douban_token_ck() 147 | if token is not None and ck is not None: 148 | result = dict(isLogin='1') 149 | else: 150 | result = dict(isLogin='0', captcha=_get_captcha()) 151 | self.write(result) 152 | 153 | 154 | class DBLoginHandler(BaseHandler): 155 | @classmethod 156 | def get_store_filename(cls): 157 | root_dir = MEDIA_ROOT + '/user/' 158 | filename = root_dir + 'douban_userinfo.json' 159 | return filename 160 | 161 | def post(self): 162 | user = self.get_argument('user') 163 | password = self.get_argument('password') 164 | token = self.get_argument('token') 165 | solution = self.get_argument('solution') 166 | token = login(user, password, token, solution) 167 | if token == 'deleted' or token is None: 168 | result = _get_captcha() 169 | result['success'] = '0' 170 | else: 171 | l = token.split('|') 172 | realtoken, ck = l[0], l[1] 173 | user_info = dict(token=realtoken, ck=ck) 174 | set_douban_token_ck(token=realtoken, ck=ck) 175 | result = dict(token=token, success='1', user=user_info) 176 | _clear_temp_folder() 177 | self.write(dict(result=result)) 178 | 179 | 180 | class DBLogoutHandler(BaseHandler): 181 | def get(self): 182 | remove_douban_token_ck() 183 | self.write(dict(result=dict(success='1'))) 184 | 185 | 186 | class DBFavoriteHandler(BaseHandler): 187 | def post(self): 188 | global manager 189 | # favorite task fetch command service 190 | command = self.get_argument('command') 191 | if command == 'start': 192 | token, ck = get_douban_token_ck() 193 | manager = FetchManager(token, ck) 194 | manager.start() 195 | status = manager.get_status() 196 | self.write(dict(result=status)) 197 | elif command == 'status': 198 | if manager is None: 199 | self.write(dict(result=dict( 200 | status='notstarted', progress=0, result=[]))) 201 | else: 202 | status = manager.get_status() 203 | self.write(dict(result=status)) 204 | -------------------------------------------------------------------------------- /listen1/handlers/home.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import logging 3 | 4 | from handlers.base import BaseHandler 5 | 6 | logger = logging.getLogger('listenone.' + __name__) 7 | 8 | 9 | class HomeHandler(BaseHandler): 10 | def get(self): 11 | self.render("index.html") 12 | -------------------------------------------------------------------------------- /listen1/handlers/player.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import logging 3 | 4 | from handlers.base import BaseHandler 5 | from replay import get_provider 6 | 7 | logger = logging.getLogger('listenone.' + __name__) 8 | 9 | 10 | class ArtistHandler(BaseHandler): 11 | def get(self): 12 | artist_id = self.get_argument('artist_id', '') 13 | provider = get_provider(artist_id) 14 | provider_name, artist_id = \ 15 | artist_id.split('_')[0], artist_id.split('_')[1] 16 | 17 | if provider_name == 'dbartist': 18 | if artist_id == '0': 19 | result = dict(status='0', reason='豆瓣暂不提供艺术家信息') 20 | self.write(result) 21 | return 22 | 23 | artist = provider.get_artist(artist_id) 24 | result = dict( 25 | status='1', 26 | tracks=artist['tracks'], 27 | info=artist['info'], 28 | is_mine='0') 29 | self.write(result) 30 | 31 | 32 | class AlbumHandler(BaseHandler): 33 | def get(self): 34 | album_id = self.get_argument('album_id', '') 35 | provider = get_provider(album_id) 36 | 37 | provider_name, album_id = \ 38 | album_id.split('_')[0], album_id.split('_')[1] 39 | 40 | if provider_name == 'dbalbum': 41 | result = dict(status='0', reason='豆瓣暂不提供专辑信息') 42 | self.write(result) 43 | return 44 | 45 | album = provider.get_album(album_id) 46 | result = dict( 47 | status='1', 48 | tracks=album['tracks'], 49 | info=album['info'], 50 | is_mine='0') 51 | self.write(result) 52 | -------------------------------------------------------------------------------- /listen1/handlers/playlist.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import logging 3 | 4 | from handlers.base import BaseHandler 5 | from models.playlist import PlaylistManager 6 | from replay import get_provider, get_provider_list 7 | 8 | logger = logging.getLogger('listenone.' + __name__) 9 | 10 | 11 | class ShowPlaylistHandler(BaseHandler): 12 | def get(self): 13 | source = self.get_argument('source', '0') 14 | 15 | provider_list = get_provider_list() 16 | index = int(source) 17 | if index >= 0 and index < len(provider_list): 18 | provider = provider_list[index] 19 | playlist = provider.list_playlist() 20 | else: 21 | playlist = [] 22 | 23 | result = dict(result=playlist) 24 | self.write(result) 25 | 26 | 27 | class PlaylistHandler(BaseHandler): 28 | def get(self): 29 | list_id = self.get_argument('list_id', '') 30 | if list_id.startswith('my_'): 31 | playlist = PlaylistManager.shared_instance().get_playlist(list_id) 32 | 33 | info = dict( 34 | cover_img_url=playlist['cover_img_url'], 35 | title=playlist['title'], id=playlist['id']) 36 | 37 | result = dict( 38 | status='1', tracks=playlist['tracks'], info=info, is_mine='1') 39 | else: 40 | provider = get_provider(list_id) 41 | item_id = list_id.split('_')[1] 42 | result = provider.get_playlist(item_id) 43 | result.update(dict(is_mine='0')) 44 | self.write(result) 45 | 46 | 47 | class AddMyPlaylistHandler(BaseHandler): 48 | def post(self): 49 | list_id = self.get_argument('list_id', '') 50 | track_id = self.get_argument('id', '') 51 | title = self.get_argument('title', '') 52 | artist = self.get_argument('artist', '') 53 | url = self.get_argument('url', '') 54 | artist_id = self.get_argument('artist_id', '') 55 | album = self.get_argument('album', '') 56 | album_id = self.get_argument('album_id', '') 57 | source = self.get_argument('source', '') 58 | source_url = self.get_argument('source_url', '') 59 | 60 | track = { 61 | 'id': track_id, 62 | 'title': title, 63 | 'artist': artist, 64 | 'url': url, 65 | 'artist_id': artist_id, 66 | 'album': album, 67 | 'album_id': album_id, 68 | 'source': source, 69 | 'source_url': source_url, 70 | } 71 | 72 | PlaylistManager.shared_instance().add_track_in_playlist(track, list_id) 73 | 74 | result = dict(result='success') 75 | 76 | self.write(result) 77 | 78 | 79 | class CreateMyPlaylistHandler(BaseHandler): 80 | def post(self): 81 | list_title = self.get_argument('list_title', '') 82 | track_id = self.get_argument('id', '') 83 | title = self.get_argument('title', '') 84 | artist = self.get_argument('artist', '') 85 | url = self.get_argument('url', '') 86 | artist_id = self.get_argument('artist_id', '') 87 | album = self.get_argument('album', '') 88 | album_id = self.get_argument('album_id', '') 89 | source = self.get_argument('source', '') 90 | source_url = self.get_argument('source_url', '') 91 | 92 | track = { 93 | 'id': track_id, 94 | 'title': title, 95 | 'artist': artist, 96 | 'url': url, 97 | 'artist_id': artist_id, 98 | 'album': album, 99 | 'album_id': album_id, 100 | 'source': source, 101 | 'source_url': source_url, 102 | } 103 | 104 | newlist_id = PlaylistManager.shared_instance()\ 105 | .create_playlist(list_title) 106 | 107 | PlaylistManager.shared_instance()\ 108 | .add_track_in_playlist(track, newlist_id) 109 | 110 | result = dict(result='success') 111 | self.write(result) 112 | 113 | 114 | class ShowMyPlaylistHandler(BaseHandler): 115 | def get(self): 116 | resultlist = PlaylistManager.shared_instance().\ 117 | list_playlist() 118 | result = dict(result=resultlist) 119 | self.write(result) 120 | 121 | 122 | class ClonePlaylistHandler(BaseHandler): 123 | def post(self): 124 | list_id = self.get_argument('list_id', '') 125 | provider = get_provider(list_id) 126 | if list_id[2:].startswith('album'): 127 | album_id = list_id.split('_')[1] 128 | album = provider.get_album(album_id) 129 | tracks = album['tracks'] 130 | info = album['info'] 131 | elif list_id[2:].startswith('artist'): 132 | artist_id = list_id.split('_')[1] 133 | artist = provider.get_artist(artist_id) 134 | tracks = artist['tracks'] 135 | info = artist['info'] 136 | elif list_id[2:].startswith('playlist'): 137 | playlist_id = list_id.split('_')[1] 138 | playlist = provider.get_playlist(playlist_id) 139 | tracks = playlist['tracks'] 140 | info = playlist['info'] 141 | 142 | list_title = info['title'] 143 | cover_img_url = info['cover_img_url'] 144 | newlist_id = PlaylistManager.shared_instance()\ 145 | .create_playlist(list_title, cover_img_url) 146 | for track in tracks: 147 | PlaylistManager.shared_instance()\ 148 | .add_track_in_playlist(track, newlist_id) 149 | result = dict(result='success') 150 | self.write(result) 151 | 152 | 153 | class RemoveTrackHandler(BaseHandler): 154 | def post(self): 155 | track_id = self.get_argument('track_id', '') 156 | list_id = self.get_argument('list_id', '') 157 | PlaylistManager.shared_instance().remove_track_in_playlist( 158 | track_id, list_id) 159 | result = dict(result='success') 160 | PlaylistManager.shared_instance() 161 | self.write(result) 162 | 163 | 164 | class RemoveMyPlaylistHandler(BaseHandler): 165 | def post(self): 166 | list_id = self.get_argument('list_id', '') 167 | PlaylistManager.shared_instance().remove_playlist(list_id) 168 | result = dict(result='success') 169 | self.write(result) 170 | -------------------------------------------------------------------------------- /listen1/handlers/search.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import logging 3 | 4 | from handlers.base import BaseHandler 5 | 6 | from replay import get_provider_list 7 | 8 | logger = logging.getLogger('listenone.' + __name__) 9 | 10 | 11 | class SearchHandler(BaseHandler): 12 | def get(self): 13 | source = self.get_argument('source', '0') 14 | keywords = self.get_argument('keywords', '') 15 | result = dict(result=[]) 16 | if keywords == '': 17 | self.write(result) 18 | 19 | provider_list = get_provider_list() 20 | 21 | index = int(source) 22 | if index >= 0 and index < len(provider_list): 23 | provider = provider_list[index] 24 | track_list = provider.search_track(keywords) 25 | else: 26 | track_list = [] 27 | 28 | result = dict(result=track_list) 29 | self.write(result) 30 | -------------------------------------------------------------------------------- /listen1/handlers/trackfile.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import errno 3 | import logging 4 | import os 5 | import os.path 6 | import time 7 | import shutil 8 | 9 | from handlers.base import BaseHandler 10 | 11 | from replay import get_provider_by_name 12 | from settings import MEDIA_ROOT 13 | 14 | import tornado.web 15 | import tornado.httpclient 16 | 17 | logger = logging.getLogger('listenone.' + __name__) 18 | 19 | 20 | class TrackFileHandler(BaseHandler): 21 | @tornado.web.asynchronous 22 | def get(self): 23 | url = self.get_argument('url', '') 24 | artist = self.get_argument('artist') 25 | album = self.get_argument('album') 26 | title = self.get_argument('title') 27 | sid = self.get_argument('id') 28 | source = self.get_argument('source', '') 29 | download = self.get_argument('download', '') 30 | 31 | provider = get_provider_by_name(source) 32 | # local cache hit test 33 | ext = 'mp3' 34 | if url != '': 35 | ext = url.split('.')[-1] 36 | else: 37 | ext = provider.filetype()[1:] 38 | 39 | file_path = os.path.join( 40 | MEDIA_ROOT, 'music', artist, album, title + 41 | '_' + sid + '.' + ext) 42 | 43 | if os.path.isfile(file_path) and download != '1': 44 | redirect_url = os.path.join( 45 | '/static/music/', artist, album, 46 | title + '_' + sid + '.' + ext) 47 | self.redirect(redirect_url, False) 48 | return 49 | 50 | if url == '': 51 | sid = sid.split('_')[1] 52 | url = provider.get_url_by_id(sid) 53 | 54 | user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) ' + \ 55 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' + \ 56 | 'Chrome/46.0.2490.86 Safari/537.36' 57 | referer = 'http://douban.fm/' 58 | xwith = 'ShockwaveFlash/19.0.0.245' 59 | headers = { 60 | 'User-Agent': user_agent, 61 | 'Referer': referer, 62 | 'X-Requested-With': xwith, 63 | 'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2', 64 | 'Accept-Encoding': 'gzip, deflate, sdch', 65 | } 66 | 67 | # read range from request header 68 | req_range = self.request.headers.get('Range', '') 69 | 70 | if req_range != '': 71 | headers['Range'] = req_range 72 | 73 | self.client = tornado.httpclient.AsyncHTTPClient() 74 | request = tornado.httpclient.HTTPRequest( 75 | url=url, headers=headers, 76 | streaming_callback=self._on_chunk, 77 | header_callback=self._on_header) 78 | 79 | self.client.fetch(request, self._on_download) 80 | 81 | # self.set_status(206) 82 | 83 | self.bytes_so_far = 0 84 | 85 | if download == '1': 86 | self.set_header('Content-Type', 'application/force-download') 87 | filename = title + '_' + artist + '.' + ext 88 | self.set_header( 89 | 'Content-Disposition', 90 | 'attachment; filename="%s"' % filename) 91 | 92 | if not os.path.exists(os.path.dirname(file_path)): 93 | try: 94 | os.makedirs(os.path.dirname(file_path)) 95 | except OSError as exc: # Guard against race condition 96 | if exc.errno != errno.EEXIST: 97 | raise 98 | timestamp_string = str(time.time()).replace('.', '') 99 | self.tmp_file_path = file_path + '.' + timestamp_string 100 | self.fd = open(self.tmp_file_path, 'wb') 101 | 102 | def _chunk(self, data): 103 | self.write(data) 104 | self.flush() 105 | 106 | def _parse_header_string(self, header_string): 107 | comma_index = header_string.find(':') 108 | k = header_string[:comma_index] 109 | v = header_string[comma_index + 1:].strip() 110 | return k, v 111 | 112 | def _on_header(self, header): 113 | k, v = self._parse_header_string(header) 114 | if k in [ 115 | 'Content-Length', 'Accept-Ranges', 116 | 'Content-Type', 'Content-Range', 117 | 'Accept-Ranges', 'Connection']: 118 | self.set_header(k, v) 119 | if header.startswith('Content-Length'): 120 | self.total_size = int(header[len('Content-Length:'):].strip()) 121 | 122 | def _on_chunk(self, chunk): 123 | self.write(chunk) 124 | self.flush() 125 | self.fd.write(chunk) 126 | self.bytes_so_far += len(chunk) 127 | 128 | def _on_download(self, response): 129 | self.finish() 130 | self.fd.close() 131 | # check if file size equals to content_length 132 | size = 0 133 | with open(self.tmp_file_path, 'r') as check_fd: 134 | check_fd.seek(0, 2) 135 | size = check_fd.tell() 136 | 137 | if size > 2 and size == self.total_size: 138 | ''' 139 | why size will less than 2: 140 | safari browser will prerequest url with byte range 2, 141 | so maybe generate temp file with 2 bytes. 142 | ''' 143 | timestamp_string = self.tmp_file_path.split('.')[-1] 144 | target_path = self.tmp_file_path[:-(len(timestamp_string) + 1)] 145 | shutil.move(self.tmp_file_path, target_path) 146 | else: 147 | os.remove(self.tmp_file_path) 148 | -------------------------------------------------------------------------------- /listen1/listen1.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | 8 | app_name = 'Listen 1' 9 | exe_name = 'Listen 1' 10 | 11 | res_path = os.path.join('.', 'res') 12 | 13 | if sys.platform == 'win32': 14 | exe_name = exe_name + '.exe' 15 | run_upx = False 16 | 17 | elif sys.platform.startswith('linux'): 18 | run_strip = True 19 | run_upx = False 20 | 21 | elif sys.platform.startswith('darwin'): 22 | run_upx = True 23 | 24 | else: 25 | print("Unsupported operating system") 26 | sys.exit(-1) 27 | 28 | added_files = [ 29 | ('media', 'media'), 30 | ('templates', 'templates') 31 | ] 32 | 33 | a = Analysis(['trayshell/launcher.py'], 34 | datas=added_files, 35 | pathex=['.'], 36 | hiddenimports=[], 37 | hookspath=None, 38 | runtime_hooks=None) 39 | 40 | pyz = PYZ(a.pure) 41 | 42 | exe = EXE(pyz, 43 | a.scripts, 44 | exclude_binaries=True, 45 | name=exe_name, 46 | debug=False, 47 | strip=None, 48 | upx=True, 49 | icon= os.path.join(res_path, 'icon.ico'), 50 | console=False) 51 | 52 | coll = COLLECT(exe, 53 | a.binaries, 54 | a.zipfiles, 55 | Tree(res_path, 'res', excludes=['*.pyc']), 56 | 57 | a.datas, 58 | strip=None, 59 | upx=run_upx, 60 | name=app_name) 61 | 62 | if sys.platform.startswith('darwin'): 63 | app = BUNDLE(coll, 64 | name='Listen 1.app', 65 | appname=exe_name, 66 | icon=os.path.join(res_path, 'icon.icns')) -------------------------------------------------------------------------------- /listen1/logconfig/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from logconfig.logconfig import initialize_logging 3 | -------------------------------------------------------------------------------- /listen1/logconfig/dictconfig.py: -------------------------------------------------------------------------------- 1 | # This is a copy of the Python logging.config.dictconfig module. It is provided 2 | # here for backwards compatibility for Python versions prior to 2.7. 3 | # 4 | # Copyright 2009-2010 by Vinay Sajip. All Rights Reserved. 5 | # 6 | # Permission to use, copy, modify, and distribute this software and its 7 | # documentation for any purpose and without fee is hereby granted, provided that 8 | # the above copyright notice appear in all copies and that both that copyright 9 | # notice and this permission notice appear in supporting documentation, and that 10 | # the name of Vinay Sajip not be used in advertising or publicity pertaining to 11 | # distribution of the software without specific, written prior permission. 12 | # VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING 13 | # ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL VINAY 14 | # SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 15 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | 19 | import logging.handlers 20 | import re 21 | import sys 22 | import types 23 | 24 | IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I) 25 | 26 | def valid_ident(s): 27 | m = IDENTIFIER.match(s) 28 | if not m: 29 | raise ValueError('Not a valid Python identifier: %r' % s) 30 | return True 31 | 32 | # 33 | # This function is defined in logging only in recent versions of Python 34 | # 35 | try: 36 | from logging import _checkLevel 37 | except ImportError: 38 | def _checkLevel(level): 39 | if isinstance(level, int): 40 | rv = level 41 | elif str(level) == level: 42 | if level not in logging._levelNames: 43 | raise ValueError('Unknown level: %r' % level) 44 | rv = logging._levelNames[level] 45 | else: 46 | raise TypeError('Level not an integer or a ' 47 | 'valid string: %r' % level) 48 | return rv 49 | 50 | # The ConvertingXXX classes are wrappers around standard Python containers, 51 | # and they serve to convert any suitable values in the container. The 52 | # conversion converts base dicts, lists and tuples to their wrapped 53 | # equivalents, whereas strings which match a conversion format are converted 54 | # appropriately. 55 | # 56 | # Each wrapper should have a configurator attribute holding the actual 57 | # configurator to use for conversion. 58 | 59 | class ConvertingDict(dict): 60 | """A converting dictionary wrapper.""" 61 | 62 | def __getitem__(self, key): 63 | value = dict.__getitem__(self, key) 64 | result = self.configurator.convert(value) 65 | #If the converted value is different, save for next time 66 | if value is not result: 67 | self[key] = result 68 | if type(result) in (ConvertingDict, ConvertingList, 69 | ConvertingTuple): 70 | result.parent = self 71 | result.key = key 72 | return result 73 | 74 | def get(self, key, default=None): 75 | value = dict.get(self, key, default) 76 | result = self.configurator.convert(value) 77 | #If the converted value is different, save for next time 78 | if value is not result: 79 | self[key] = result 80 | if type(result) in (ConvertingDict, ConvertingList, 81 | ConvertingTuple): 82 | result.parent = self 83 | result.key = key 84 | return result 85 | 86 | def pop(self, key, default=None): 87 | value = dict.pop(self, key, default) 88 | result = self.configurator.convert(value) 89 | if value is not result: 90 | if type(result) in (ConvertingDict, ConvertingList, 91 | ConvertingTuple): 92 | result.parent = self 93 | result.key = key 94 | return result 95 | 96 | class ConvertingList(list): 97 | """A converting list wrapper.""" 98 | def __getitem__(self, key): 99 | value = list.__getitem__(self, key) 100 | result = self.configurator.convert(value) 101 | #If the converted value is different, save for next time 102 | if value is not result: 103 | self[key] = result 104 | if type(result) in (ConvertingDict, ConvertingList, 105 | ConvertingTuple): 106 | result.parent = self 107 | result.key = key 108 | return result 109 | 110 | def pop(self, idx=-1): 111 | value = list.pop(self, idx) 112 | result = self.configurator.convert(value) 113 | if value is not result: 114 | if type(result) in (ConvertingDict, ConvertingList, 115 | ConvertingTuple): 116 | result.parent = self 117 | return result 118 | 119 | class ConvertingTuple(tuple): 120 | """A converting tuple wrapper.""" 121 | def __getitem__(self, key): 122 | value = tuple.__getitem__(self, key) 123 | result = self.configurator.convert(value) 124 | if value is not result: 125 | if type(result) in (ConvertingDict, ConvertingList, 126 | ConvertingTuple): 127 | result.parent = self 128 | result.key = key 129 | return result 130 | 131 | class BaseConfigurator(object): 132 | """ 133 | The configurator base class which defines some useful defaults. 134 | """ 135 | 136 | CONVERT_PATTERN = re.compile(r'^(?P[a-z]+)://(?P.*)$') 137 | 138 | WORD_PATTERN = re.compile(r'^\s*(\w+)\s*') 139 | DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*') 140 | INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*') 141 | DIGIT_PATTERN = re.compile(r'^\d+$') 142 | 143 | value_converters = { 144 | 'ext' : 'ext_convert', 145 | 'cfg' : 'cfg_convert', 146 | } 147 | 148 | # We might want to use a different one, e.g. importlib 149 | importer = __import__ 150 | 151 | def __init__(self, config): 152 | self.config = ConvertingDict(config) 153 | self.config.configurator = self 154 | 155 | def resolve(self, s): 156 | """ 157 | Resolve strings to objects using standard import and attribute 158 | syntax. 159 | """ 160 | name = s.split('.') 161 | used = name.pop(0) 162 | found = self.importer(used) 163 | for frag in name: 164 | used += '.' + frag 165 | try: 166 | found = getattr(found, frag) 167 | except AttributeError: 168 | self.importer(used) 169 | found = getattr(found, frag) 170 | return found 171 | 172 | def ext_convert(self, value): 173 | """Default converter for the ext:// protocol.""" 174 | return self.resolve(value) 175 | 176 | def cfg_convert(self, value): 177 | """Default converter for the cfg:// protocol.""" 178 | rest = value 179 | m = self.WORD_PATTERN.match(rest) 180 | if m is None: 181 | raise ValueError("Unable to convert %r" % value) 182 | else: 183 | rest = rest[m.end():] 184 | d = self.config[m.groups()[0]] 185 | #print d, rest 186 | while rest: 187 | m = self.DOT_PATTERN.match(rest) 188 | if m: 189 | d = d[m.groups()[0]] 190 | else: 191 | m = self.INDEX_PATTERN.match(rest) 192 | if m: 193 | idx = m.groups()[0] 194 | if not self.DIGIT_PATTERN.match(idx): 195 | d = d[idx] 196 | else: 197 | try: 198 | n = int(idx) # try as number first (most likely) 199 | d = d[n] 200 | except TypeError: 201 | d = d[idx] 202 | if m: 203 | rest = rest[m.end():] 204 | else: 205 | raise ValueError('Unable to convert ' 206 | '%r at %r' % (value, rest)) 207 | #rest should be empty 208 | return d 209 | 210 | def convert(self, value): 211 | """ 212 | Convert values to an appropriate type. dicts, lists and tuples are 213 | replaced by their converting alternatives. Strings are checked to 214 | see if they have a conversion format and are converted if they do. 215 | """ 216 | if not isinstance(value, ConvertingDict) and isinstance(value, dict): 217 | value = ConvertingDict(value) 218 | value.configurator = self 219 | elif not isinstance(value, ConvertingList) and isinstance(value, list): 220 | value = ConvertingList(value) 221 | value.configurator = self 222 | elif not isinstance(value, ConvertingTuple) and\ 223 | isinstance(value, tuple): 224 | value = ConvertingTuple(value) 225 | value.configurator = self 226 | elif isinstance(value, str): # str for py3k 227 | m = self.CONVERT_PATTERN.match(value) 228 | if m: 229 | d = m.groupdict() 230 | prefix = d['prefix'] 231 | converter = self.value_converters.get(prefix, None) 232 | if converter: 233 | suffix = d['suffix'] 234 | converter = getattr(self, converter) 235 | value = converter(suffix) 236 | return value 237 | 238 | def configure_custom(self, config): 239 | """Configure an object with a user-supplied factory.""" 240 | c = config.pop('()') 241 | if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType: 242 | c = self.resolve(c) 243 | props = config.pop('.', None) 244 | # Check for valid identifiers 245 | kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) 246 | result = c(**kwargs) 247 | if props: 248 | for name, value in props.items(): 249 | setattr(result, name, value) 250 | return result 251 | 252 | def as_tuple(self, value): 253 | """Utility function which converts lists to tuples.""" 254 | if isinstance(value, list): 255 | value = tuple(value) 256 | return value 257 | 258 | class DictConfigurator(BaseConfigurator): 259 | """ 260 | Configure logging using a dictionary-like object to describe the 261 | configuration. 262 | """ 263 | 264 | def configure(self): 265 | """Do the configuration.""" 266 | 267 | config = self.config 268 | if 'version' not in config: 269 | raise ValueError("dictionary doesn't specify a version") 270 | if config['version'] != 1: 271 | raise ValueError("Unsupported version: %s" % config['version']) 272 | incremental = config.pop('incremental', False) 273 | EMPTY_DICT = {} 274 | logging._acquireLock() 275 | try: 276 | if incremental: 277 | handlers = config.get('handlers', EMPTY_DICT) 278 | # incremental handler config only if handler name 279 | # ties in to logging._handlers (Python 2.7) 280 | if sys.version_info[:2] == (2, 7): 281 | for name in handlers: 282 | if name not in logging._handlers: 283 | raise ValueError('No handler found with ' 284 | 'name %r' % name) 285 | else: 286 | try: 287 | handler = logging._handlers[name] 288 | handler_config = handlers[name] 289 | level = handler_config.get('level', None) 290 | if level: 291 | handler.setLevel(_checkLevel(level)) 292 | except StandardError as e: 293 | raise ValueError('Unable to configure handler ' 294 | '%r: %s' % (name, e)) 295 | loggers = config.get('loggers', EMPTY_DICT) 296 | for name in loggers: 297 | try: 298 | self.configure_logger(name, loggers[name], True) 299 | except StandardError as e: 300 | raise ValueError('Unable to configure logger ' 301 | '%r: %s' % (name, e)) 302 | root = config.get('root', None) 303 | if root: 304 | try: 305 | self.configure_root(root, True) 306 | except StandardError as e: 307 | raise ValueError('Unable to configure root ' 308 | 'logger: %s' % e) 309 | else: 310 | disable_existing = config.pop('disable_existing_loggers', True) 311 | 312 | logging._handlers.clear() 313 | del logging._handlerList[:] 314 | 315 | # Do formatters first - they don't refer to anything else 316 | formatters = config.get('formatters', EMPTY_DICT) 317 | for name in formatters: 318 | try: 319 | formatters[name] = self.configure_formatter( 320 | formatters[name]) 321 | except StandardError as e: 322 | raise ValueError('Unable to configure ' 323 | 'formatter %r: %s' % (name, e)) 324 | # Next, do filters - they don't refer to anything else, either 325 | filters = config.get('filters', EMPTY_DICT) 326 | for name in filters: 327 | try: 328 | filters[name] = self.configure_filter(filters[name]) 329 | except StandardError as e: 330 | raise ValueError('Unable to configure ' 331 | 'filter %r: %s' % (name, e)) 332 | 333 | # Next, do handlers - they refer to formatters and filters 334 | # As handlers can refer to other handlers, sort the keys 335 | # to allow a deterministic order of configuration 336 | handlers = config.get('handlers', EMPTY_DICT) 337 | for name in sorted(handlers): 338 | try: 339 | handler = self.configure_handler(handlers[name]) 340 | handler.name = name 341 | handlers[name] = handler 342 | except StandardError as e: 343 | raise ValueError('Unable to configure handler ' 344 | '%r: %s' % (name, e)) 345 | # Next, do loggers - they refer to handlers and filters 346 | 347 | #we don't want to lose the existing loggers, 348 | #since other threads may have pointers to them. 349 | #existing is set to contain all existing loggers, 350 | #and as we go through the new configuration we 351 | #remove any which are configured. At the end, 352 | #what's left in existing is the set of loggers 353 | #which were in the previous configuration but 354 | #which are not in the new configuration. 355 | root = logging.root 356 | existing = root.manager.loggerDict.keys() 357 | #The list needs to be sorted so that we can 358 | #avoid disabling child loggers of explicitly 359 | #named loggers. With a sorted list it is easier 360 | #to find the child loggers. 361 | sorted(existing) 362 | #We'll keep the list of existing loggers 363 | #which are children of named loggers here... 364 | child_loggers = [] 365 | #now set up the new ones... 366 | loggers = config.get('loggers', EMPTY_DICT) 367 | for name in loggers: 368 | if name in existing: 369 | i = existing.index(name) 370 | prefixed = name + "." 371 | pflen = len(prefixed) 372 | num_existing = len(existing) 373 | i = i + 1 # look at the entry after name 374 | while (i < num_existing) and\ 375 | (existing[i][:pflen] == prefixed): 376 | child_loggers.append(existing[i]) 377 | i = i + 1 378 | existing.remove(name) 379 | try: 380 | self.configure_logger(name, loggers[name]) 381 | except StandardError as e: 382 | raise ValueError('Unable to configure logger ' 383 | '%r: %s' % (name, e)) 384 | 385 | #Disable any old loggers. There's no point deleting 386 | #them as other threads may continue to hold references 387 | #and by disabling them, you stop them doing any logging. 388 | #However, don't disable children of named loggers, as that's 389 | #probably not what was intended by the user. 390 | for log in existing: 391 | logger = root.manager.loggerDict[log] 392 | if log in child_loggers: 393 | logger.level = logging.NOTSET 394 | logger.handlers = [] 395 | logger.propagate = True 396 | elif disable_existing: 397 | logger.disabled = True 398 | 399 | # And finally, do the root logger 400 | root = config.get('root', None) 401 | if root: 402 | try: 403 | self.configure_root(root) 404 | except StandardError as e: 405 | raise ValueError('Unable to configure root ' 406 | 'logger: %s' % e) 407 | finally: 408 | logging._releaseLock() 409 | 410 | def configure_formatter(self, config): 411 | """Configure a formatter from a dictionary.""" 412 | if '()' in config: 413 | factory = config['()'] # for use in exception handler 414 | try: 415 | result = self.configure_custom(config) 416 | except TypeError as te: 417 | if "'format'" not in str(te): 418 | raise 419 | #Name of parameter changed from fmt to format. 420 | #Retry with old name. 421 | #This is so that code can be used with older Python versions 422 | #(e.g. by Django) 423 | config['fmt'] = config.pop('format') 424 | config['()'] = factory 425 | result = self.configure_custom(config) 426 | else: 427 | fmt = config.get('format', None) 428 | dfmt = config.get('datefmt', None) 429 | result = logging.Formatter(fmt, dfmt) 430 | return result 431 | 432 | def configure_filter(self, config): 433 | """Configure a filter from a dictionary.""" 434 | if '()' in config: 435 | result = self.configure_custom(config) 436 | else: 437 | name = config.get('name', '') 438 | result = logging.Filter(name) 439 | return result 440 | 441 | def add_filters(self, filterer, filters): 442 | """Add filters to a filterer from a list of names.""" 443 | for f in filters: 444 | try: 445 | filterer.addFilter(self.config['filters'][f]) 446 | except StandardError as e: 447 | raise ValueError('Unable to add filter %r: %s' % (f, e)) 448 | 449 | def configure_handler(self, config): 450 | """Configure a handler from a dictionary.""" 451 | formatter = config.pop('formatter', None) 452 | if formatter: 453 | try: 454 | formatter = self.config['formatters'][formatter] 455 | except StandardError as e: 456 | raise ValueError('Unable to set formatter ' 457 | '%r: %s' % (formatter, e)) 458 | level = config.pop('level', None) 459 | filters = config.pop('filters', None) 460 | if '()' in config: 461 | c = config.pop('()') 462 | if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType: 463 | c = self.resolve(c) 464 | factory = c 465 | else: 466 | klass = self.resolve(config.pop('class')) 467 | #Special case for handler which refers to another handler 468 | if issubclass(klass, logging.handlers.MemoryHandler) and\ 469 | 'target' in config: 470 | try: 471 | config['target'] = self.config['handlers'][config['target']] 472 | except StandardError as e: 473 | raise ValueError('Unable to set target handler ' 474 | '%r: %s' % (config['target'], e)) 475 | elif issubclass(klass, logging.handlers.SMTPHandler) and\ 476 | 'mailhost' in config: 477 | config['mailhost'] = self.as_tuple(config['mailhost']) 478 | elif issubclass(klass, logging.handlers.SysLogHandler) and\ 479 | 'address' in config: 480 | config['address'] = self.as_tuple(config['address']) 481 | factory = klass 482 | kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) 483 | try: 484 | result = factory(**kwargs) 485 | except TypeError as te: 486 | if "'stream'" not in str(te): 487 | raise 488 | #The argument name changed from strm to stream 489 | #Retry with old name. 490 | #This is so that code can be used with older Python versions 491 | #(e.g. by Django) 492 | kwargs['strm'] = kwargs.pop('stream') 493 | result = factory(**kwargs) 494 | if formatter: 495 | result.setFormatter(formatter) 496 | if level is not None: 497 | result.setLevel(_checkLevel(level)) 498 | if filters: 499 | self.add_filters(result, filters) 500 | return result 501 | 502 | def add_handlers(self, logger, handlers): 503 | """Add handlers to a logger from a list of names.""" 504 | for h in handlers: 505 | try: 506 | logger.addHandler(self.config['handlers'][h]) 507 | except StandardError as e: 508 | raise ValueError('Unable to add handler %r: %s' % (h, e)) 509 | 510 | def common_logger_config(self, logger, config, incremental=False): 511 | """ 512 | Perform configuration which is common to root and non-root loggers. 513 | """ 514 | level = config.get('level', None) 515 | if level is not None: 516 | logger.setLevel(_checkLevel(level)) 517 | if not incremental: 518 | #Remove any existing handlers 519 | for h in logger.handlers[:]: 520 | logger.removeHandler(h) 521 | handlers = config.get('handlers', None) 522 | if handlers: 523 | self.add_handlers(logger, handlers) 524 | filters = config.get('filters', None) 525 | if filters: 526 | self.add_filters(logger, filters) 527 | 528 | def configure_logger(self, name, config, incremental=False): 529 | """Configure a non-root logger from a dictionary.""" 530 | logger = logging.getLogger(name) 531 | self.common_logger_config(logger, config, incremental) 532 | propagate = config.get('propagate', None) 533 | if propagate is not None: 534 | logger.propagate = propagate 535 | 536 | def configure_root(self, config, incremental=False): 537 | """Configure a root logger from a dictionary.""" 538 | root = logging.getLogger() 539 | self.common_logger_config(root, config, incremental) 540 | 541 | dictConfigClass = DictConfigurator 542 | 543 | def dictConfig(config): 544 | """Configure logging using a dictionary.""" 545 | dictConfigClass(config).configure() 546 | -------------------------------------------------------------------------------- /listen1/logconfig/logconfig.py: -------------------------------------------------------------------------------- 1 | """An extended version of the log_settings module from zamboni: 2 | https://github.com/jbalogh/zamboni/blob/master/log_settings.py 3 | """ 4 | from __future__ import absolute_import 5 | 6 | from tornado.log import LogFormatter as TornadoLogFormatter 7 | import logging, logging.handlers 8 | import os.path 9 | import types 10 | 11 | from logconfig import dictconfig 12 | 13 | # Pulled from commonware.log we don't have to import that, which drags with 14 | # it Django dependencies. 15 | class RemoteAddressFormatter(logging.Formatter): 16 | """Formatter that makes sure REMOTE_ADDR is available.""" 17 | 18 | def format(self, record): 19 | if ('%(REMOTE_ADDR)' in self._fmt 20 | and 'REMOTE_ADDR' not in record.__dict__): 21 | record.__dict__['REMOTE_ADDR'] = None 22 | return logging.Formatter.format(self, record) 23 | 24 | class UTF8SafeFormatter(RemoteAddressFormatter): 25 | def __init__(self, fmt=None, datefmt=None, encoding='utf-8'): 26 | logging.Formatter.__init__(self, fmt, datefmt) 27 | self.encoding = encoding 28 | 29 | def formatException(self, e): 30 | r = logging.Formatter.formatException(self, e) 31 | if type(r) in [types.StringType]: 32 | r = r.decode(self.encoding, 'replace') # Convert to unicode 33 | return r 34 | 35 | def format(self, record): 36 | t = RemoteAddressFormatter.format(self, record) 37 | if type(t) in [types.UnicodeType]: 38 | t = t.encode(self.encoding, 'replace') 39 | return t 40 | 41 | class NullHandler(logging.Handler): 42 | def emit(self, record): 43 | pass 44 | 45 | def initialize_logging(syslog_tag, syslog_facility, loggers, 46 | log_level=logging.INFO, use_syslog=False): 47 | if os.path.exists('/dev/log'): 48 | syslog_device = '/dev/log' 49 | elif os.path.exists('/var/run/syslog'): 50 | syslog_device = '/var/run/syslog' 51 | else: 52 | syslog_device = None 53 | base_fmt = ('%(name)s:%(levelname)s %(message)s:%(pathname)s:%(lineno)s') 54 | 55 | cfg = { 56 | 'version': 1, 57 | 'filters': {}, 58 | 'formatters': { 59 | 'debug': { 60 | '()': UTF8SafeFormatter, 61 | 'datefmt': '%H:%M:%s', 62 | 'format': '%(asctime)s ' + base_fmt, 63 | }, 64 | 'prod': { 65 | '()': UTF8SafeFormatter, 66 | 'datefmt': '%H:%M:%s', 67 | 'format': '%s: [%%(REMOTE_ADDR)s] %s' % (syslog_tag, base_fmt), 68 | }, 69 | 'tornado': { 70 | '()': TornadoLogFormatter, 71 | 'color': True 72 | }, 73 | }, 74 | 'handlers': { 75 | 'console': { 76 | '()': logging.StreamHandler, 77 | 'formatter': 'tornado' 78 | }, 79 | 'null': { 80 | '()': NullHandler, 81 | }, 82 | 'syslog': { 83 | '()': logging.handlers.SysLogHandler, 84 | 'facility': syslog_facility, 85 | 'address': syslog_device, 86 | 'formatter': 'prod', 87 | }, 88 | }, 89 | 'loggers': { 90 | 'listenone':{}, 91 | } 92 | } 93 | 94 | for key, value in loggers.items(): 95 | cfg[key].update(value) 96 | 97 | # Set the level and handlers for all loggers. 98 | for logger in cfg['loggers'].values(): 99 | if 'handlers' not in logger: 100 | logger['handlers'] = ['syslog' if use_syslog else 'console'] 101 | if 'level' not in logger: 102 | logger['level'] = log_level 103 | if 'propagate' not in logger: 104 | logger['propagate'] = False 105 | 106 | dictconfig.dictConfig(cfg) 107 | -------------------------------------------------------------------------------- /listen1/make.sh: -------------------------------------------------------------------------------- 1 | 2 | rm ./media/temp/* 3 | rm -rf ./media/music/* 4 | rm ./media/user/* 5 | rm -rf ./build ./dist 6 | rm -rf ../package_resource/mac/dmg-resource/Listen\ 1.app 7 | 8 | pyinstaller listen1.spec --clean -y 9 | 10 | cp -r ./dist/Listen\ 1.app ../package_resource/mac/dmg-resource/ 11 | 12 | cd ../package_resource/mac/dmg-resource/ 13 | 14 | rm ../../../dist/listen1_mac.dmg 15 | appdmg listen1.json ../../../dist/listen1_mac.dmg 16 | rm -rf ./Listen\ 1.app 17 | -------------------------------------------------------------------------------- /listen1/media/css/angular-ui-notification.css: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating 3 | * @author Alex_Crack 4 | * @version v0.1.0 5 | * @link https://github.com/alexcrack/angular-ui-notification 6 | * @license MIT 7 | */ 8 | .ui-notification 9 | { 10 | position: fixed; 11 | z-index: 9999; 12 | 13 | width: 300px; 14 | 15 | cursor: pointer; 16 | -webkit-transition: all ease .5s; 17 | -o-transition: all ease .5s; 18 | transition: all ease .5s; 19 | 20 | color: #fff; 21 | border-radius: 0; 22 | background: #337ab7; 23 | box-shadow: 5px 5px 10px rgba(0, 0, 0, .3); 24 | } 25 | .ui-notification.killed 26 | { 27 | -webkit-transition: opacity ease 1s; 28 | -o-transition: opacity ease 1s; 29 | transition: opacity ease 1s; 30 | 31 | opacity: 0; 32 | } 33 | .ui-notification > h3 34 | { 35 | font-size: 14px; 36 | font-weight: bold; 37 | 38 | display: block; 39 | 40 | margin: 10px 10px 0 10px; 41 | padding: 0 0 5px 0; 42 | 43 | text-align: left; 44 | 45 | border-bottom: 1px solid rgba(255, 255, 255, .3); 46 | } 47 | .ui-notification a 48 | { 49 | color: #fff; 50 | } 51 | .ui-notification a:hover 52 | { 53 | text-decoration: underline; 54 | } 55 | .ui-notification > .message 56 | { 57 | margin: 10px 10px 10px 10px; 58 | } 59 | .ui-notification.warning 60 | { 61 | color: #fff; 62 | background: #f0ad4e; 63 | } 64 | .ui-notification.error 65 | { 66 | color: #fff; 67 | background: #d9534f; 68 | } 69 | .ui-notification.success 70 | { 71 | color: #fff; 72 | background: #5cb85c; 73 | } 74 | .ui-notification.info 75 | { 76 | color: #fff; 77 | background: #5bc0de; 78 | } 79 | .ui-notification:hover 80 | { 81 | opacity: .7; 82 | } 83 | -------------------------------------------------------------------------------- /listen1/media/css/cover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | /* Links */ 6 | a, 7 | a:focus, 8 | a:hover { 9 | color: #fff; 10 | } 11 | 12 | /* Custom default button */ 13 | .btn-default, 14 | .btn-default:hover, 15 | .btn-default:focus { 16 | color: #333; 17 | text-shadow: none; /* Prevent inheritence from `body` */ 18 | background-color: #fff; 19 | border: 1px solid #fff; 20 | } 21 | 22 | 23 | /* 24 | * Base structure 25 | */ 26 | 27 | html, 28 | body { 29 | height: 100%; 30 | background-color: #333; 31 | } 32 | body { 33 | color: #fff; 34 | /* text-align: center;*/ 35 | /*text-shadow: 0 1px 3px rgba(0,0,0,.5);*/ 36 | } 37 | 38 | /* Extra markup and styles for table-esque vertical and horizontal centering */ 39 | .site-wrapper { 40 | display: table; 41 | width: 100%; 42 | height: 100%; /* For at least Firefox */ 43 | min-height: 100%; 44 | /* -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); 45 | box-shadow: inset 0 0 100px rgba(0,0,0,.5);*/ 46 | } 47 | .site-wrapper-inner { 48 | display: table-cell; 49 | vertical-align: top; 50 | } 51 | .cover-container { 52 | margin-right: auto; 53 | margin-left: auto; 54 | } 55 | 56 | /* Padding for spacing */ 57 | .inner { 58 | padding: 30px; 59 | } 60 | 61 | 62 | /* 63 | * Header 64 | */ 65 | .masthead-brand { 66 | margin-top: 10px; 67 | margin-bottom: 10px; 68 | } 69 | 70 | .masthead-nav > li { 71 | display: inline-block; 72 | } 73 | .masthead-nav > li + li { 74 | margin-left: 20px; 75 | } 76 | .masthead-nav > li > a { 77 | padding-right: 0; 78 | padding-left: 0; 79 | font-size: 16px; 80 | font-weight: bold; 81 | color: #fff; /* IE8 proofing */ 82 | color: rgba(255,255,255,.75); 83 | border-bottom: 2px solid transparent; 84 | } 85 | .masthead-nav > li > a:hover, 86 | .masthead-nav > li > a:focus { 87 | background-color: transparent; 88 | border-bottom-color: #a9a9a9; 89 | border-bottom-color: rgba(255,255,255,.25); 90 | } 91 | .masthead-nav > .active > a, 92 | .masthead-nav > .active > a:hover, 93 | .masthead-nav > .active > a:focus { 94 | color: #fff; 95 | border-bottom-color: #fff; 96 | } 97 | 98 | @media (min-width: 768px) { 99 | .masthead-brand { 100 | float: left; 101 | } 102 | .masthead-nav { 103 | float: right; 104 | } 105 | } 106 | 107 | 108 | /* 109 | * Cover 110 | */ 111 | 112 | .cover { 113 | padding: 0 20px; 114 | } 115 | .cover .btn-lg { 116 | padding: 10px 20px; 117 | font-weight: bold; 118 | } 119 | 120 | 121 | /* 122 | * Footer 123 | */ 124 | 125 | .mastfoot { 126 | color: #999; /* IE8 proofing */ 127 | color: rgba(255,255,255,.5); 128 | } 129 | 130 | 131 | /* 132 | * Affix and center 133 | */ 134 | 135 | @media (min-width: 768px) { 136 | /* Pull out the header and footer */ 137 | .masthead { 138 | position: fixed; 139 | top: 0; 140 | } 141 | .mastfoot { 142 | position: fixed; 143 | bottom: 0; 144 | } 145 | /* Start the vertical centering */ 146 | .site-wrapper-inner { 147 | vertical-align: middle; 148 | } 149 | /* Handle the widths */ 150 | .masthead, 151 | .mastfoot, 152 | .cover-container { 153 | width: 100%; /* Must be percentage or pixels for horizontal alignment */ 154 | } 155 | } 156 | 157 | @media (min-width: 992px) { 158 | .masthead, 159 | .mastfoot, 160 | .cover-container { 161 | width: 880px; 162 | } 163 | } -------------------------------------------------------------------------------- /listen1/media/css/player.css: -------------------------------------------------------------------------------- 1 | a { 2 | cursor: pointer; 3 | } 4 | 5 | .shadow { 6 | position: fixed; 7 | background: rgba(30,30,30,0.9); 8 | _position: absolute; 9 | z-index: 9999; 10 | top: 0; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | width: 100%; 15 | height: 100%; 16 | background-image: url(data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==); 17 | } 18 | 19 | .dialog { 20 | position: absolute; 21 | top: 120px; 22 | width: 480px; 23 | height: 420px; 24 | z-index:10000; 25 | overflow: hidden; 26 | border-radius: 4px 4px 4px 4px; 27 | padding: 20px; 28 | background-color: #333; 29 | 30 | } 31 | 32 | .dialog-header { 33 | width: 100%; 34 | height: 30px; 35 | font-family: Arial, Helvetica, sans-serif; 36 | font-size: 15px; 37 | font-weight: bold; 38 | text-align: left; 39 | border-bottom: 1px solid; 40 | margin-bottom: 20px; 41 | } 42 | 43 | .dialog-header .dialog-close { 44 | float: right; 45 | font-size: 33px; 46 | cursor: pointer; 47 | margin-top: -15px; 48 | } 49 | 50 | .dialog-body { 51 | width: 100%; 52 | height: 320px; 53 | overflow-y: auto; 54 | background-color: #333; 55 | } 56 | 57 | /*.masthead { 58 | z-index: 999; 59 | }*/ 60 | 61 | .masthead, .mastfoot { 62 | margin: 0 auto; 63 | left: 0; 64 | right: 0; 65 | z-index: 999; 66 | background-color: #222222; 67 | } 68 | 69 | .masthead { 70 | background-color: #333; 71 | height: 90px; 72 | } 73 | 74 | .masthead .logo { 75 | float: left; 76 | height: 50px; 77 | width: 50px; 78 | margin-right: 20px; 79 | } 80 | 81 | .cover-container { 82 | position: relative; 83 | } 84 | 85 | .site-wrapper { 86 | width:100%; 87 | overflow:hidden; 88 | position: absolute; 89 | padding-left: 17px; 90 | } 91 | 92 | .site-wrapper-innerd { 93 | overflow-y: scroll; 94 | margin-top: 90px; 95 | /* uncomment the line below will hide the scroll bar */ 96 | /*padding-right: 17px;*/ 97 | box-sizing:content-box; 98 | width:100%; 99 | background-color: #333; 100 | } 101 | 102 | .searchbox { 103 | /* margin-top: 100px;*/ 104 | } 105 | 106 | .searchbox .nav{ 107 | margin-top: 12px; 108 | } 109 | 110 | 111 | .searchitem { 112 | height: 92px; 113 | } 114 | 115 | .searchitem img { 116 | float: left; 117 | height:90px; 118 | width:90px; 119 | } 120 | 121 | .searchitem div { 122 | float: left; 123 | margin-left: 48px; 124 | margin-top: 38px; 125 | width: 400px; 126 | } 127 | 128 | .playlist-covers li { 129 | float: left; 130 | display: inline-block; 131 | width: 140px; 132 | height: 188px; 133 | margin-right: 22px; 134 | } 135 | 136 | .playlist-covers .desc { 137 | text-align: left; 138 | } 139 | 140 | .playlist-covers .u-cover { 141 | position: relative; 142 | display: block; 143 | width: 140px; 144 | height: 140px; 145 | } 146 | 147 | .playlist-covers .u-cover .bottom { 148 | position: absolute; 149 | bottom: 0; 150 | left: 0; 151 | width: 100%; 152 | height: 27px; 153 | color: #ccc; 154 | } 155 | 156 | .u-cover .mask { 157 | position: absolute; 158 | top: 0; 159 | left: 0; 160 | width: 100%; 161 | height: 100%; 162 | } 163 | 164 | .u-cover img { 165 | display: block; 166 | width: 100%; 167 | height: 100%; 168 | } 169 | 170 | .u-cover .icon-play { 171 | position: absolute; 172 | right: 10px; 173 | bottom: 5px; 174 | width: 16px; 175 | height: 17px; 176 | background: url(/static/images/player_directplay.png); 177 | } 178 | 179 | .playlist-covers .u-cover .bottom .nb { 180 | float: left; 181 | margin: 7px 0 0 0; 182 | } 183 | 184 | .m-playbar { 185 | position: absolute; 186 | zoom: 1; 187 | top: -90px; 188 | left: 0; 189 | width: 100%; 190 | height: 90px; 191 | margin: 0 auto; 192 | background-color: #222222; 193 | } 194 | 195 | 196 | .m-playbar .btns { 197 | width: 157px; 198 | padding: 27px 0 0 19px; 199 | } 200 | 201 | .m-playbar .btns, .m-playbar .head, .m-playbar .play, .m-playbar .volum, .m-playbar .oper { 202 | float: left; 203 | } 204 | 205 | .m-pbar .btn i { 206 | visibility: hidden; 207 | position: absolute; 208 | left: 5px; 209 | top: 5px; 210 | width: 12px; 211 | height: 12px; 212 | background: url(/static/images/loading.gif); 213 | } 214 | 215 | .m-pbar .barbg, .m-pbar .cur, .m-pbar .left { 216 | background: url(/static/images/statbar.png) no-repeat 0 9999px; 217 | _background-image: url(/static/images/statbar.png); 218 | } 219 | 220 | .m-playbar .btns a { 221 | background: url(/static/images/player_large.png) no-repeat 0 9999px; 222 | _background-image: url(/static/images/player_large.png); 223 | cursor: pointer; 224 | } 225 | 226 | .m-playbar .btns a { 227 | display: block; 228 | float: left; 229 | width: 36px; 230 | height: 36px; 231 | margin-right: 8px; 232 | margin-top: 0px; 233 | text-indent: -9999px; 234 | } 235 | 236 | .m-playbar .btns .previous{ 237 | background-position: -72px 0px; 238 | } 239 | 240 | .m-playbar .btns .previous:hover{ 241 | background-position:-72px -36px; 242 | } 243 | 244 | .m-playbar .btns .play{ 245 | width: 36px; 246 | height: 36px; 247 | margin-top:0; 248 | background-position: 0px 0px; 249 | } 250 | 251 | .m-playbar .btns .play:hover{ 252 | background-position: 0px -36px; 253 | } 254 | 255 | .m-playbar .btns .pas{ 256 | background-position: -108px 0px; 257 | } 258 | 259 | .m-playbar .btns .pas:hover{ 260 | background-position: -108px -36px; 261 | } 262 | 263 | .m-playbar .btns .next{ 264 | /* pause icon distance adjust from 36 to 38 */ 265 | background-position: -38px 0px; 266 | } 267 | 268 | .m-playbar .btns .next:hover{ 269 | background-position: -38px -36px; 270 | } 271 | 272 | .m-playbar .head { 273 | position: relative; 274 | margin: 10px 15px 0 0; 275 | } 276 | 277 | .m-playbar .head, .m-playbar .head img { 278 | width: 70px; 279 | height: 70px; 280 | } 281 | 282 | .m-playbar .head .mask { 283 | position: absolute; 284 | top: 0px; 285 | left: 0px; 286 | display: block; 287 | width: 70px; 288 | height: 70px; 289 | } 290 | 291 | .m-playbar .maininfo { 292 | float: none; 293 | margin-left: 245px; 294 | margin-right: 120px; 295 | } 296 | 297 | .m-playbar .words .notextdeco{ 298 | text-decoration: none; 299 | } 300 | 301 | .m-playbar .words { 302 | margin-top: 14px; 303 | height: 28px; 304 | overflow: hidden; 305 | color: #e8e8e8; 306 | text-shadow: 0 1px 0 #171717; 307 | line-height: 28px; 308 | } 309 | 310 | .m-playbar .words .name { 311 | max-width: 300px; 312 | } 313 | 314 | .m-playbar .words .by { 315 | max-width: 220px; 316 | margin-left: 15px; 317 | color: #9b9b9b; 318 | } 319 | 320 | .m-playbar .words .by a { 321 | color: #9b9b9b; 322 | } 323 | 324 | .m-playbar .words .src { 325 | cursor: pointer; 326 | float: left; 327 | width: 25px; 328 | height: 25px; 329 | margin: 2px 0 0 13px; 330 | background: url(/static/images/player_small.png) no-repeat 0 9999px; 331 | background-position: -100px 0px; 332 | } 333 | 334 | .m-playbar .words .src:hover { 335 | background-position: -100px -25px; 336 | } 337 | 338 | .m-playbar .words .fc1 { 339 | color: #e8e8e8; 340 | margin-left: 3px; 341 | } 342 | 343 | .overflowhide { 344 | overflow: hidden; 345 | text-overflow: ellipsis; 346 | white-space: nowrap; 347 | } 348 | 349 | .floatleft { 350 | float: left; 351 | } 352 | 353 | .m-pbar { 354 | position: relative; 355 | margin-top: 14px; 356 | width: 80%; 357 | } 358 | 359 | .m-pbar .barbg, .m-pbar .cur, .m-pbar .rdy { 360 | height: 7px; 361 | } 362 | 363 | .m-pbar .barbg { 364 | background-position: right 0; 365 | } 366 | 367 | 368 | .m-pbar .cur { 369 | position: absolute; 370 | top: 0; 371 | left: 0; 372 | width: 1%; 373 | background-position: left -9px; 374 | } 375 | 376 | .m-pbar .btn { 377 | position: absolute; 378 | top: -8px; 379 | right: -13px; 380 | width: 22px; 381 | height: 24px; 382 | margin-left: -11px; 383 | background: url(/static/images/progress_indicator.png) no-repeat; 384 | } 385 | 386 | .m-playbar .time { 387 | position: absolute; 388 | right: -122px; 389 | top: -6px; 390 | } 391 | 392 | .m-playbar .time { 393 | float: right; 394 | margin-right: 20px; 395 | color: #797979; 396 | text-shadow: 0 1px 0 #121212; 397 | } 398 | 399 | .m-playbar .time em { 400 | color: #a1a1a1; 401 | } 402 | 403 | em, i { 404 | font-style: normal; 405 | text-align: left; 406 | font-size: inherit; 407 | } 408 | 409 | .m-playbar a { 410 | background: url(/static/images/player_small.png) no-repeat 0 9999px; 411 | } 412 | 413 | .m-playbar .ctrl { 414 | position: absolute; 415 | right: 0px; 416 | bottom: 17px; 417 | z-index: 10; 418 | width: 110px; 419 | padding-left: 13px; 420 | float: none; 421 | } 422 | 423 | 424 | 425 | .m-playbar .icn-add { 426 | background-position: -25px 0px; 427 | } 428 | 429 | .m-playbar .icn-add:hover { 430 | background-position: -25px -25px; 431 | } 432 | 433 | .m-playbar .icn-list { 434 | background-position: -125px 0px; 435 | } 436 | 437 | .m-playbar .icn-list:hover { 438 | background-position: -125px -25px; 439 | } 440 | 441 | .m-playbar .icn-loop { 442 | background-position: -50px 0px; 443 | } 444 | 445 | .m-playbar .icn-loop:hover { 446 | background-position: -50px -25px; 447 | } 448 | 449 | 450 | .m-playbar .icn-shuffle { 451 | background-position: -150px 0px; 452 | } 453 | 454 | .m-playbar .icn-shuffle:hover { 455 | background-position: -150px -25px; 456 | } 457 | 458 | 459 | .m-playbar .icn { 460 | float: left; 461 | width: 25px; 462 | height: 25px; 463 | margin: 11px 2px 0 0; 464 | text-indent: -9999px; 465 | } 466 | 467 | .m-playbar .menu { 468 | position: absolute; 469 | bottom: 90px; 470 | _bottom: 90px; 471 | right: 0px; 472 | _right: 0px; 473 | width: 60%; 474 | height: 301px; 475 | background-color: #121212; 476 | } 477 | 478 | .m-playbar .menu ul { 479 | padding-left: 0px; 480 | height: 270px; 481 | overflow: auto; 482 | } 483 | 484 | .m-playbar .menu li { 485 | float: left; 486 | width: 100%; 487 | display: block; 488 | } 489 | 490 | .m-playbar .menu .playing { 491 | background-color: #555555; 492 | } 493 | 494 | .m-playbar .menu li:hover, .m-playbar .menu li:focus { 495 | background-color: #999999; 496 | } 497 | 498 | .m-playbar .menu .icn-remove { 499 | height: 25px; 500 | width: 25px; 501 | background-position: -75px -25px; 502 | display: inline-block; 503 | } 504 | 505 | .m-playbar .menu .icn-remove:hover { 506 | background-position: -75px -25px; 507 | } 508 | 509 | li { 510 | list-style: none; 511 | } 512 | 513 | .menu-header { 514 | height: 28px; 515 | background-color: #121212; 516 | padding-top: 4px; 517 | text-align: center; 518 | } 519 | 520 | .menu .remove-all { 521 | float:right; 522 | margin-right: 10px; 523 | } 524 | 525 | .menu .title { 526 | width: 300px; 527 | float: left; 528 | height: 28px; 529 | padding-top: 3px; 530 | text-align: left; 531 | padding-left: 20px; 532 | overflow: hidden; 533 | white-space: nowrap; 534 | text-overflow: ellipsis; 535 | cursor: pointer; 536 | } 537 | 538 | .menu .singer { 539 | width: 180px; 540 | float: right; 541 | height: 28px; 542 | padding-top: 3px; 543 | text-align: left; 544 | padding-left: 20px; 545 | overflow: hidden; 546 | white-space: nowrap; 547 | text-overflow: ellipsis; 548 | cursor: pointer; 549 | } 550 | 551 | .dbimport { 552 | /*margin-top: 100px;*/ 553 | } 554 | 555 | .form-signin { 556 | width: 300px; 557 | margin-left: auto; 558 | margin-right: auto; 559 | text-align: center; 560 | } 561 | 562 | .form-signin .form-control, .form-signin .valid-img, .form-signin .btn { 563 | margin-top: 10px; 564 | } 565 | 566 | .form-signin .valid-img { 567 | height: 40px; 568 | width: 220px; 569 | } 570 | 571 | .form-signin .security-notice { 572 | margin-top: 10px; 573 | } 574 | 575 | .playlist-detail { 576 | position: absolute; 577 | text-align: left; 578 | background-color: #333; 579 | width: 100%; 580 | } 581 | 582 | .playlist-detail .detail-head { 583 | width: 200px; 584 | position: fixed; 585 | margin-bottom: 20px; 586 | } 587 | 588 | .playlist-detail .detail-head-cover { 589 | height: 180px; 590 | /* width: 225px;*/ 591 | float: left; 592 | margin: 10px; 593 | } 594 | 595 | .playlist-detail .detail-head-cover img { 596 | max-width:100%; 597 | max-height:100%; 598 | } 599 | 600 | .playlist-detail .detail-head-title { 601 | float: left; 602 | width: 100%; 603 | text-align: center; 604 | } 605 | 606 | .playlist-detail .detail-head-title .play { 607 | display: inline-block; 608 | text-indent: -9999px; 609 | width: 36px; 610 | height: 36px; 611 | margin-right: 8px; 612 | margin-top: 0; 613 | background-position-x: 0; 614 | background: url(/static/images/player_large.png) no-repeat 0 9999px; 615 | background-position: 0px 0px; 616 | } 617 | 618 | .playlist-detail .detail-head-title .play:hover { 619 | background: url(/static/images/player_large.png) no-repeat 0 9999px; 620 | background-position: 0px -36px; 621 | } 622 | 623 | .playlist-detail .detail-head-title .delete { 624 | display: inline-block; 625 | text-indent: -9999px; 626 | width: 36px; 627 | height: 36px; 628 | margin-right: 8px; 629 | margin-top: 0; 630 | background: url(/static/images/player_large.png) no-repeat 0 9999px; 631 | background-position: -180px 0px; 632 | } 633 | 634 | .playlist-detail .detail-head-title .delete:hover { 635 | background: url(/static/images/player_large.png) no-repeat 0 9999px; 636 | background-position: -180px -36px; 637 | } 638 | 639 | .playlist-detail .detail-head-title .clone { 640 | display: inline-block; 641 | text-indent: -9999px; 642 | width: 36px; 643 | height: 36px; 644 | margin-right: 8px; 645 | margin-top: 0; 646 | background: url(/static/images/player_large.png) no-repeat 0 9999px; 647 | background-position: -144px 0px; 648 | } 649 | 650 | .playlist-detail .detail-head-title .clone:hover { 651 | background: url(/static/images/player_large.png) no-repeat 0 9999px; 652 | background-position: -144px -36px; 653 | } 654 | 655 | .playlist-detail .detail-head-title .ply:hover { 656 | background-position: -40px -204px; 657 | } 658 | 659 | .playlist-detail .detail-head-title h2{ 660 | font-size: 17px; 661 | margin-bottom: 35px; 662 | } 663 | 664 | .playlist-detail .detail-songlist { 665 | margin-left: 220px; 666 | margin-top: 6px; 667 | margin-right: 14px; 668 | } 669 | 670 | .detail-songlist { 671 | padding-left: 0px; 672 | text-align: left; 673 | } 674 | 675 | .detail-songlist li { 676 | float: left; 677 | width: 100%; 678 | display: block; 679 | padding: 8px; 680 | } 681 | 682 | .detail-songlist .col2 { 683 | float: left; 684 | width: 28%; 685 | margin-left: 2%; 686 | font-size: 15px; 687 | } 688 | 689 | .detail-songlist .col1 { 690 | float: left; 691 | width: 19%; 692 | margin-left: 2%; 693 | } 694 | 695 | .detail-songlist .col-add { 696 | float: left; 697 | width: 75px; 698 | margin-left: 2%; 699 | } 700 | 701 | .detail-songlist .detail-tools { 702 | float: right; 703 | height: 35px; 704 | position: relative; 705 | width: 118px; 706 | } 707 | 708 | .detail-songlist .detail-tools a { 709 | background: url(/static/images/player_small.png) no-repeat 0 9999px; 710 | height: 25px; 711 | width: 25px; 712 | cursor: pointer; 713 | } 714 | 715 | .detail-songlist .detail-tools .detail-add-button{ 716 | background-position: 0px 0px; 717 | } 718 | 719 | .detail-songlist .detail-tools .detail-fav-button{ 720 | background-position: -25px 0px; 721 | } 722 | 723 | .detail-songlist .detail-tools .detail-delete-button{ 724 | background-position: -75px 0px; 725 | } 726 | 727 | .detail-songlist .detail-tools .source-button{ 728 | background-position: -100px 0px; 729 | } 730 | 731 | .detail-songlist .detail-tools .detail-add-button:hover{ 732 | background-position: 0px -25px; 733 | } 734 | 735 | .detail-songlist .detail-tools .detail-fav-button:hover{ 736 | background-position: -25px -25px; 737 | } 738 | 739 | .detail-songlist .detail-tools .detail-delete-button:hover{ 740 | background-position: -75px -25px; 741 | } 742 | 743 | .detail-songlist .detail-tools .source-button:hover{ 744 | background-position: -100px -25px; 745 | } 746 | 747 | .detail-songlist .detail-tools a { 748 | text-decoration: none; 749 | display: inline-block; 750 | } 751 | 752 | .detail-songlist .detail-artist a{ 753 | color: #777777; 754 | } 755 | 756 | .detail-songlist .odd { 757 | background-color: #333; 758 | } 759 | 760 | .detail-songlist .even, .detail-songlist .detail-add{ 761 | background-color: #2d2d2d; 762 | } 763 | 764 | .dialog .detail-songlist li:hover { 765 | background-color: #999999; 766 | cursor: pointer; 767 | } 768 | 769 | /*.playlist-detail .detail-songlist li:hover { 770 | background-color: #999999; 771 | }*/ 772 | 773 | .playlist-detail .btn{ 774 | width: 88px; 775 | margin-top: 0; 776 | float: left; 777 | } 778 | 779 | .playlist-detail .detail-close { 780 | position: absolute; 781 | right: -32px; 782 | top: 0px; 783 | } 784 | 785 | .playlist-detail .detail-close span { 786 | font-size: 34px; 787 | cursor: pointer; 788 | color: #aaaaaa; 789 | } 790 | 791 | .playlist-detail .detail-close span:hover { 792 | color: #ffffff; 793 | } 794 | 795 | .dialog-playlist { 796 | padding-left: 0px; 797 | text-align: left; 798 | } 799 | 800 | .dialog-playlist li { 801 | cursor: pointer; 802 | height: 112px; 803 | padding: 6px; 804 | } 805 | 806 | .dialog-playlist li:hover { 807 | background-color: #555555; 808 | } 809 | 810 | .dialog-playlist li img { 811 | float: left; 812 | height: 100px; 813 | width: 100px; 814 | } 815 | 816 | .dialog-playlist li h2{ 817 | margin-left: 125px; 818 | font-size: 17px; 819 | } 820 | 821 | .dialog-newplaylist input{ 822 | margin-bottom: 22px; 823 | } 824 | 825 | .dialog-newplaylist .confirm-button { 826 | margin-right: 64px; 827 | } 828 | 829 | .source-list { 830 | position: absolute; 831 | right: -32px; 832 | top: 0px; 833 | } 834 | 835 | .source-list button{ 836 | background-color: #333333; 837 | color: #fff; 838 | } 839 | 840 | .settings-title { 841 | font-size: 20px; 842 | padding: 20px; 843 | border-bottom: 2px solid #aaaaaa; 844 | } 845 | 846 | .settings-content { 847 | padding: 20px; 848 | } -------------------------------------------------------------------------------- /listen1/media/css/reset.css: -------------------------------------------------------------------------------- 1 | html, body, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, font, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | dl, dt, dd, ol, ul, li, 7 | fieldset, form, label, legend { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | outline: 0; 12 | font-weight: normal; 13 | font-style: normal; 14 | font-size: 100%; 15 | vertical-align: baseline; 16 | } 17 | 18 | : focus { 19 | outline: 0; 20 | } 21 | 22 | body { 23 | line-height: 1.2; 24 | color: black; 25 | background: white; 26 | } 27 | 28 | h1, 29 | h2, 30 | h3, 31 | h4, 32 | h5, 33 | h6 { 34 | font-size: 100%; 35 | font-weight: normal; 36 | } 37 | 38 | ol, ul { 39 | list-style: none; 40 | } 41 | 42 | /* tables still need 'cellspacing="0"' in the markup */ 43 | table { 44 | border-collapse: separate; 45 | border-spacing: 0; 46 | } 47 | 48 | caption, th, td { 49 | text-align: left; 50 | font-weight: normal; 51 | } 52 | 53 | blockquote:before, blockquote:after, 54 | q:before, q:after { 55 | content: ""; 56 | } 57 | 58 | blockquote, q { 59 | quotes: "" ""; 60 | } 61 | 62 | img { 63 | border: none; 64 | } 65 | -------------------------------------------------------------------------------- /listen1/media/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/favicon.ico -------------------------------------------------------------------------------- /listen1/media/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/loading.gif -------------------------------------------------------------------------------- /listen1/media/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/logo.png -------------------------------------------------------------------------------- /listen1/media/images/mycover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/mycover.jpg -------------------------------------------------------------------------------- /listen1/media/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/placeholder.png -------------------------------------------------------------------------------- /listen1/media/images/playbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/playbar.png -------------------------------------------------------------------------------- /listen1/media/images/player_directplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/player_directplay.png -------------------------------------------------------------------------------- /listen1/media/images/player_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/player_large.png -------------------------------------------------------------------------------- /listen1/media/images/player_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/player_small.png -------------------------------------------------------------------------------- /listen1/media/images/progress_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/progress_indicator.png -------------------------------------------------------------------------------- /listen1/media/images/statbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/media/images/statbar.png -------------------------------------------------------------------------------- /listen1/media/js/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | Storage.prototype.setObject = function(key, value) { 5 | this.setItem(key, JSON.stringify(value)); 6 | } 7 | 8 | Storage.prototype.getObject = function(key) { 9 | var value = this.getItem(key); 10 | return value && JSON.parse(value); 11 | } 12 | 13 | var app = angular.module('listenone', ['angularSoundManager', 'ui-notification']); 14 | 15 | app.config(function(NotificationProvider) { 16 | NotificationProvider.setOptions({ 17 | delay: 2000, 18 | startTop: 20, 19 | startRight: 10, 20 | verticalSpacing: 20, 21 | horizontalSpacing: 20, 22 | positionX: 'center', 23 | positionY: 'top' 24 | }); 25 | }); 26 | 27 | // control main view of page, it can be called any place 28 | app.controller('NavigationController', ['$scope', '$http', 29 | '$httpParamSerializerJQLike', '$timeout', 30 | 'angularPlayer', 'Notification', '$rootScope', 31 | function($scope, $http, $httpParamSerializerJQLike, 32 | $timeout, angularPlayer, Notification, $rootScope){ 33 | $scope.window_url_stack = []; 34 | $scope.current_tag = 1; 35 | $scope.is_window_hidden = 1; 36 | $scope.is_dialog_hidden = 1; 37 | 38 | $scope.songs = []; 39 | $scope.current_list_id = -1; 40 | 41 | $scope.dialog_song = ''; 42 | $scope.dialog_type = 0; 43 | $scope.dialog_title = ''; 44 | 45 | $scope.isDoubanLogin = false; 46 | $scope.$on('isdoubanlogin:update', function(event, data) { 47 | $scope.isDoubanLogin = data; 48 | }); 49 | 50 | // tag 51 | $scope.showTag = function(tag_id){ 52 | $scope.current_tag = tag_id; 53 | $scope.is_window_hidden = 1; 54 | $scope.window_url_stack = []; 55 | $scope.closeWindow(); 56 | }; 57 | 58 | // playlist window 59 | $scope.resetWindow = function() { 60 | $scope.cover_img_url = '/static/images/loading.gif'; 61 | $scope.playlist_title = ''; 62 | $scope.songs = []; 63 | }; 64 | 65 | $scope.showWindow = function(url){ 66 | $scope.is_window_hidden = 0; 67 | $scope.resetWindow(); 68 | 69 | $scope.window_url_stack.push(url); 70 | $http.get(url).success(function(data) { 71 | if (data.status == '0') { 72 | Notification.info(data.reason); 73 | $scope.popWindow(); 74 | return; 75 | } 76 | $scope.songs = data.tracks; 77 | $scope.cover_img_url = data.info.cover_img_url; 78 | $scope.playlist_title = data.info.title; 79 | $scope.list_id = data.info.id; 80 | $scope.is_mine = data.is_mine; 81 | }); 82 | }; 83 | 84 | $scope.closeWindow = function(){ 85 | $scope.is_window_hidden = 1; 86 | $scope.resetWindow(); 87 | $scope.window_url_stack = []; 88 | }; 89 | 90 | $scope.popWindow = function() { 91 | $scope.window_url_stack.pop(); 92 | if($scope.window_url_stack.length === 0) { 93 | $scope.closeWindow(); 94 | } 95 | else { 96 | $scope.resetWindow(); 97 | var url = $scope.window_url_stack[$scope.window_url_stack.length-1]; 98 | $http.get(url).success(function(data) { 99 | $scope.songs = data.tracks; 100 | $scope.cover_img_url = data.info.cover_img_url; 101 | $scope.playlist_title = data.info.title; 102 | }); 103 | } 104 | }; 105 | 106 | $scope.showPlaylist = function(list_id) { 107 | var url = '/playlist?list_id=' + list_id; 108 | $scope.showWindow(url); 109 | }; 110 | 111 | $scope.showArtist = function(artist_id) { 112 | var url = '/artist?artist_id=' + artist_id; 113 | $scope.showWindow(url); 114 | }; 115 | 116 | $scope.showAlbum = function(album_id) { 117 | var url = '/album?album_id=' + album_id; 118 | $scope.showWindow(url); 119 | }; 120 | 121 | $scope.directplaylist = function(list_id){ 122 | var url = '/playlist?list_id=' + list_id; 123 | 124 | $http.get(url).success(function(data) { 125 | $scope.songs = data.tracks; 126 | $scope.current_list_id = list_id; 127 | 128 | $timeout(function(){ 129 | // use timeout to avoid stil in digest error. 130 | angularPlayer.clearPlaylist(function(data) { 131 | //add songs to playlist 132 | angularPlayer.addTrackArray($scope.songs); 133 | //play first song 134 | var index = 0; 135 | if (angularPlayer.getShuffle()) { 136 | var max = $scope.songs.length - 1; 137 | var min = 0; 138 | index = Math.floor(Math.random() * (max - min + 1)) + min; 139 | } 140 | angularPlayer.playTrack($scope.songs[index].id); 141 | 142 | }); 143 | }, 0); 144 | }); 145 | }; 146 | 147 | $scope.showDialog = function(dialog_type, data) { 148 | $scope.is_dialog_hidden = 0; 149 | var dialogWidth = 480; 150 | var left = $(window).width()/2 - dialogWidth/2; 151 | $scope.myStyle = {'left': left + 'px'}; 152 | 153 | if (dialog_type == 0) { 154 | $scope.dialog_title = '添加到歌单'; 155 | var url = '/show_myplaylist'; 156 | $scope.dialog_song = data; 157 | $http.get(url).success(function(data) { 158 | $scope.myplaylist = data.result; 159 | }); 160 | } 161 | 162 | if (dialog_type == 2) { 163 | $scope.dialog_title = '登录豆瓣'; 164 | $scope.dialog_type = 2; 165 | } 166 | }; 167 | 168 | $scope.chooseDialogOption = function(option_id) { 169 | var url = '/add_myplaylist'; 170 | 171 | $http({ 172 | url: url, 173 | method: 'POST', 174 | data: $httpParamSerializerJQLike({ 175 | list_id: option_id, 176 | id: $scope.dialog_song.id, 177 | title: $scope.dialog_song.title, 178 | artist: $scope.dialog_song.artist, 179 | url: $scope.dialog_song.url, 180 | artist_id: $scope.dialog_song.artist_id, 181 | album: $scope.dialog_song.album, 182 | album_id: $scope.dialog_song.album_id, 183 | source: $scope.dialog_song.source, 184 | source_url: $scope.dialog_song.source_url 185 | }), 186 | headers: { 187 | 'Content-Type': 'application/x-www-form-urlencoded' 188 | } 189 | }).success(function() { 190 | Notification.success('添加到歌单成功'); 191 | $scope.closeDialog(); 192 | // add to current playing list 193 | if (option_id == $scope.current_list_id) { 194 | angularPlayer.addTrack($scope.dialog_song); 195 | } 196 | }); 197 | }; 198 | 199 | $scope.newDialogOption = function() { 200 | $scope.dialog_type = 1; 201 | }; 202 | 203 | $scope.cancelNewDialog = function() { 204 | $scope.dialog_type = 0; 205 | }; 206 | 207 | $scope.createAndAddPlaylist = function() { 208 | var url = '/create_myplaylist'; 209 | 210 | $http({ 211 | url: url, 212 | method: 'POST', 213 | data: $httpParamSerializerJQLike({ 214 | list_title: $scope.newlist_title, 215 | id: $scope.dialog_song.id, 216 | title: $scope.dialog_song.title, 217 | artist: $scope.dialog_song.artist, 218 | url: $scope.dialog_song.url, 219 | artist_id: $scope.dialog_song.artist_id, 220 | album: $scope.dialog_song.album, 221 | album_id: $scope.dialog_song.album_id, 222 | source: $scope.dialog_song.source, 223 | source_url: $scope.dialog_song.source_url 224 | }), 225 | headers: { 226 | 'Content-Type': 'application/x-www-form-urlencoded' 227 | } 228 | }).success(function() { 229 | $rootScope.$broadcast('myplaylist:update'); 230 | Notification.success('添加到歌单成功'); 231 | $scope.closeDialog(); 232 | }); 233 | }; 234 | 235 | $scope.removeSongFromPlaylist = function(song, list_id) { 236 | var url = '/remove_track_from_myplaylist'; 237 | 238 | $http({ 239 | url: url, 240 | method: 'POST', 241 | data: $httpParamSerializerJQLike({ 242 | list_id: list_id, 243 | track_id: song.id 244 | }), 245 | headers: { 246 | 'Content-Type': 'application/x-www-form-urlencoded' 247 | } 248 | }).success(function() { 249 | // remove song from songs 250 | var index = $scope.songs.indexOf(song); 251 | if (index > -1) { 252 | $scope.songs.splice(index, 1); 253 | } 254 | Notification.success('删除成功'); 255 | }); 256 | } 257 | 258 | $scope.closeDialog = function() { 259 | $scope.is_dialog_hidden = 1; 260 | $scope.dialog_type = 0; 261 | }; 262 | 263 | $scope.setCurrentList = function(list_id) { 264 | $scope.current_list_id = list_id; 265 | }; 266 | 267 | $scope.$on('player:playlist', function(event, data) { 268 | localStorage.setObject('current-playing', data); 269 | }); 270 | 271 | $scope.$on('track:id', function(event, data) { 272 | var current = localStorage.getObject('player-settings'); 273 | current.nowplaying_track_id = data; 274 | localStorage.setObject('player-settings', current); 275 | }); 276 | }]); 277 | 278 | 279 | app.controller('PlayController', ['$scope', '$timeout','$log', 280 | '$anchorScroll', '$location', 'angularPlayer', '$http', 281 | '$httpParamSerializerJQLike','$rootScope', 'Notification', 282 | function($scope, $timeout, $log, $anchorScroll, $location, angularPlayer, 283 | $http, $httpParamSerializerJQLike, $rootScope, Notification){ 284 | $scope.menuHidden = true; 285 | 286 | $scope.settings = {"playmode": 0, "nowplaying_track_id": -1}; 287 | 288 | $scope.loadLocalSettings = function() { 289 | var defaultSettings = {"playmode": 0, "nowplaying_track_id": -1} 290 | var localSettings = localStorage.getObject('player-settings'); 291 | if (localSettings == null) { 292 | $scope.settings = {"playmode": 0, "nowplaying_track_id": -1}; 293 | $scope.saveLocalSettings(); 294 | } 295 | else { 296 | $scope.settings = localSettings; 297 | } 298 | // apply settings 299 | var shuffleSetting; 300 | if ($scope.settings.playmode == 1) { 301 | shuffleSetting = true; 302 | } 303 | else { 304 | shuffleSetting = false; 305 | } 306 | if (angularPlayer.getShuffle() != shuffleSetting) { 307 | angularPlayer.toggleShuffle(); 308 | } 309 | } 310 | 311 | $scope.saveLocalSettings = function() { 312 | localStorage.setObject('player-settings', $scope.settings); 313 | } 314 | 315 | $scope.loadLocalCurrentPlaying = function() { 316 | var localSettings = localStorage.getObject('current-playing'); 317 | if (localSettings == null) { 318 | return; 319 | } 320 | // apply local current playing; 321 | angularPlayer.addTrackArray(localSettings); 322 | } 323 | 324 | $scope.saveLocalCurrentPlaying = function() { 325 | localStorage.setObjct('current-playing', angularPlayer.playlist) 326 | } 327 | 328 | $scope.changePlaymode = function() { 329 | // loop: 0, shuffle: 1 330 | angularPlayer.toggleShuffle(); 331 | if (angularPlayer.getShuffle()) { 332 | $scope.settings.playmode = 1; 333 | } 334 | else { 335 | $scope.settings.playmode = 0; 336 | } 337 | $scope.saveLocalSettings(); 338 | }; 339 | 340 | $scope.$on('angularPlayer:ready', function(event, data) { 341 | $log.debug('cleared, ok now add to playlist'); 342 | if (angularPlayer.getRepeatStatus() == false) { 343 | angularPlayer.repeatToggle(); 344 | } 345 | //add songs to playlist 346 | var localCurrentPlaying = localStorage.getObject('current-playing'); 347 | if (localCurrentPlaying == null) { 348 | return; 349 | } 350 | angularPlayer.addTrackArray(localCurrentPlaying); 351 | 352 | var localPlayerSettings = localStorage.getObject('player-settings'); 353 | if (localPlayerSettings == null) { 354 | return; 355 | } 356 | var track_id = localPlayerSettings.nowplaying_track_id; 357 | if (track_id != -1) { 358 | angularPlayer.playTrack(track_id); 359 | } 360 | else { 361 | angularPlayer.play(); 362 | } 363 | // disable open and play feature 364 | angularPlayer.pause(); 365 | }); 366 | 367 | $scope.gotoAnchor = function(newHash) { 368 | if ($location.hash() !== newHash) { 369 | // set the $location.hash to `newHash` and 370 | // $anchorScroll will automatically scroll to it 371 | $location.hash(newHash); 372 | $anchorScroll(); 373 | } else { 374 | // call $anchorScroll() explicitly, 375 | // since $location.hash hasn't changed 376 | $anchorScroll(); 377 | } 378 | }; 379 | 380 | $scope.togglePlaylist = function() { 381 | var anchor = "song" + angularPlayer.getCurrentTrack(); 382 | $scope.menuHidden = !$scope.menuHidden; 383 | if (!$scope.menuHidden) { 384 | $scope.gotoAnchor(anchor); 385 | } 386 | }; 387 | 388 | $scope.playmylist = function(list_id){ 389 | $timeout(function(){ 390 | angularPlayer.clearPlaylist(function(data) { 391 | //add songs to playlist 392 | angularPlayer.addTrackArray($scope.songs); 393 | var index = 0; 394 | if (angularPlayer.getShuffle()) { 395 | var max = $scope.songs.length - 1; 396 | var min = 0; 397 | index = Math.floor(Math.random() * (max - min + 1)) + min; 398 | } 399 | //play first song 400 | angularPlayer.playTrack($scope.songs[index].id); 401 | }); 402 | }, 0); 403 | $scope.setCurrentList(list_id); 404 | }; 405 | 406 | $scope.removemylist = function(list_id){ 407 | var url = '/remove_myplaylist'; 408 | 409 | $http({ 410 | url: url, 411 | method: 'POST', 412 | data: $httpParamSerializerJQLike({ 413 | list_id: list_id, 414 | }), 415 | headers: { 416 | 'Content-Type': 'application/x-www-form-urlencoded' 417 | } 418 | }).success(function() { 419 | $rootScope.$broadcast('myplaylist:update'); 420 | $scope.closeWindow(); 421 | Notification.success('删除成功'); 422 | }); 423 | }; 424 | 425 | $scope.clonelist = function(list_id){ 426 | var url = '/clone_playlist'; 427 | 428 | $http({ 429 | url: url, 430 | method: 'POST', 431 | data: $httpParamSerializerJQLike({ 432 | list_id: list_id, 433 | }), 434 | headers: { 435 | 'Content-Type': 'application/x-www-form-urlencoded' 436 | } 437 | }).success(function() { 438 | $rootScope.$broadcast('myplaylist:update'); 439 | $scope.closeWindow(); 440 | Notification.success('收藏到我的歌单成功'); 441 | }); 442 | }; 443 | 444 | $scope.myProgress = 0; 445 | $scope.changingProgress = false; 446 | 447 | $rootScope.$on('track:progress', function(event, data) { 448 | if ($scope.changingProgress == false) { 449 | $scope.myProgress = data; 450 | } 451 | }); 452 | 453 | $rootScope.$on('track:myprogress', function(event, data) { 454 | $scope.$apply(function() { 455 | // should use apply to force refresh ui 456 | $scope.myProgress = data; 457 | }); 458 | }); 459 | }]); 460 | 461 | app.controller('InstantSearchController', ['$scope', '$http', '$timeout', 'angularPlayer', 462 | function($scope, $http, $timeout, angularPlayer) { 463 | $scope.tab = 0; 464 | $scope.keywords = ''; 465 | 466 | $scope.changeTab = function(newTab){ 467 | $scope.tab = newTab; 468 | $scope.result = []; 469 | $http.get('/search?source='+ $scope.tab + '&keywords=' + $scope.keywords).success(function(data) { 470 | // update the textarea 471 | $scope.result = data.result; 472 | }); 473 | }; 474 | 475 | $scope.isActiveTab = function(tab){ 476 | return $scope.tab === tab; 477 | }; 478 | 479 | $scope.$watch('keywords', function (tmpStr) { 480 | if (!tmpStr || tmpStr.length === 0){ 481 | $scope.result = []; 482 | return 0; 483 | } 484 | // if searchStr is still the same.. 485 | // go ahead and retrieve the data 486 | if (tmpStr === $scope.keywords) 487 | { 488 | $http.get('/search?source='+ $scope.tab + '&keywords=' + $scope.keywords).success(function(data) { 489 | // update the textarea 490 | $scope.result = data.result; 491 | }); 492 | } 493 | }); 494 | }]); 495 | 496 | app.directive('errSrc', function() { 497 | // http://stackoverflow.com/questions/16310298/if-a-ngsrc-path-resolves-to-a-404-is-there-a-way-to-fallback-to-a-default 498 | return { 499 | link: function(scope, element, attrs) { 500 | element.bind('error', function() { 501 | if (attrs.src != attrs.errSrc) { 502 | attrs.$set('src', attrs.errSrc); 503 | } 504 | }); 505 | attrs.$observe('ngSrc', function(value) { 506 | if (!value && attrs.errSrc) { 507 | attrs.$set('src', attrs.errSrc); 508 | } 509 | }); 510 | } 511 | } 512 | }); 513 | 514 | app.directive('resize', function ($window) { 515 | return function (scope, element) { 516 | var w = angular.element($window); 517 | var changeHeight = function(){ 518 | var headerHeight = 90; 519 | var footerHeight = 90; 520 | element.css('height', (w.height() - headerHeight - footerHeight) + 'px' ); 521 | }; 522 | w.bind('resize', function () { 523 | changeHeight(); // when window size gets changed 524 | }); 525 | changeHeight(); // when page loads 526 | }; 527 | }); 528 | 529 | app.directive('addAndPlay', ['angularPlayer', function (angularPlayer) { 530 | return { 531 | restrict: "EA", 532 | scope: { 533 | song: "=addAndPlay" 534 | }, 535 | link: function (scope, element, attrs) { 536 | element.bind('click', function (event) { 537 | angularPlayer.addTrack(scope.song); 538 | angularPlayer.playTrack(scope.song.id); 539 | }); 540 | } 541 | }; 542 | }]); 543 | 544 | app.directive('addWithoutPlay', ['angularPlayer', 'Notification', 545 | function (angularPlayer, Notification) { 546 | return { 547 | restrict: "EA", 548 | scope: { 549 | song: "=addWithoutPlay" 550 | }, 551 | link: function (scope, element, attrs) { 552 | element.bind('click', function (event) { 553 | angularPlayer.addTrack(scope.song); 554 | Notification.success("已添加到当前播放歌单"); 555 | }); 556 | } 557 | }; 558 | }]); 559 | 560 | app.directive('openSongSource', ['angularPlayer', '$window', 561 | function (angularPlayer, $window) { 562 | return { 563 | restrict: "EA", 564 | scope: { 565 | song: "=openSongSource" 566 | }, 567 | link: function (scope, element, attrs) { 568 | element.bind('click', function (event) { 569 | $window.open(scope.song.source_url, '_blank'); 570 | }); 571 | } 572 | }; 573 | }]); 574 | 575 | app.directive('draggable', ['angularPlayer', '$document', '$rootScope', 576 | function(angularPlayer, $document, $rootScope) { 577 | return function(scope, element, attr) { 578 | var x; 579 | var container; 580 | 581 | element.on('mousedown', function(event) { 582 | scope.changingProgress = true; 583 | container = document.getElementById('progressbar').getBoundingClientRect(); 584 | // Prevent default dragging of selected content 585 | event.preventDefault(); 586 | x = event.clientX - container.left; 587 | setPosition(); 588 | $document.on('mousemove', mousemove); 589 | $document.on('mouseup', mouseup); 590 | 591 | }); 592 | 593 | function mousemove(event) { 594 | x = event.clientX - container.left; 595 | setPosition(); 596 | } 597 | 598 | function changeProgress(progress) { 599 | if (angularPlayer.getCurrentTrack() === null) { 600 | return; 601 | } 602 | var sound = soundManager.getSoundById(angularPlayer.getCurrentTrack()); 603 | var duration = sound.durationEstimate; 604 | sound.setPosition(progress * duration); 605 | } 606 | 607 | function setPosition() { 608 | if (container) { 609 | if (x < 0) { 610 | x = 0; 611 | } else if (x > container.right - container.left) { 612 | x = container.right - container.left; 613 | } 614 | } 615 | var progress = x / (container.right - container.left); 616 | $rootScope.$broadcast('track:myprogress', progress*100); 617 | } 618 | 619 | function mouseup() { 620 | var progress = x / (container.right - container.left); 621 | changeProgress(progress); 622 | $document.off('mousemove', mousemove); 623 | $document.off('mouseup', mouseup); 624 | scope.changingProgress = false; 625 | } 626 | }; 627 | }]); 628 | 629 | app.controller('MyPlayListController', ['$http','$scope', '$timeout', 630 | 'angularPlayer', function($http, $scope, $timeout, angularPlayer){ 631 | $scope.myplaylists = []; 632 | 633 | $scope.loadMyPlaylist = function(){ 634 | $http.get('/show_myplaylist').success(function(data) { 635 | $scope.myplaylists = data.result; 636 | }); 637 | }; 638 | 639 | $scope.$watch('current_tag', function(newValue, oldValue) { 640 | if (newValue !== oldValue) { 641 | if (newValue == '1') { 642 | $scope.myplaylists = []; 643 | $scope.loadMyPlaylist(); 644 | } 645 | } 646 | }); 647 | $scope.$on('myplaylist:update', function(event, data) { 648 | $scope.loadMyPlaylist(); 649 | }); 650 | 651 | }]); 652 | 653 | app.controller('PlayListController', ['$http','$scope', '$timeout', 654 | 'angularPlayer', function($http, $scope, $timeout, angularPlayer){ 655 | $scope.result = []; 656 | 657 | $scope.tab = 0; 658 | 659 | $scope.changeTab = function(newTab){ 660 | $scope.tab = newTab; 661 | $scope.result = []; 662 | $http.get('/show_playlist?source=' + $scope.tab).success(function(data) { 663 | $scope.result = data.result; 664 | }); 665 | }; 666 | 667 | $scope.isActiveTab = function(tab){ 668 | return $scope.tab === tab; 669 | }; 670 | 671 | 672 | $scope.loadPlaylist = function(){ 673 | $http.get('/show_playlist?source=' + $scope.tab).success(function(data) { 674 | $scope.result = data.result; 675 | }); 676 | }; 677 | }]); 678 | 679 | app.controller('ImportController', ['$http', 680 | '$httpParamSerializerJQLike', '$scope', '$interval', 681 | '$timeout', '$rootScope', 'Notification', 'angularPlayer', 682 | function($http, $httpParamSerializerJQLike, $scope, 683 | $interval, $timeout, $rootScope, Notification, angularPlayer){ 684 | $scope.validcode_url = ""; 685 | $scope.token = ""; 686 | 687 | $scope.getLoginInfo = function(){ 688 | $http.get('/dbvalidcode').success(function(data) { 689 | if (data.isLogin == 0) { 690 | $scope.validcode_url = data.captcha.path; 691 | $scope.token = data.captcha.token; 692 | } 693 | else { 694 | // already login 695 | $scope.isDoubanLogin = true; 696 | $rootScope.$broadcast('isdoubanlogin:update', true); 697 | } 698 | }); 699 | }; 700 | 701 | $scope.loginDouban = function(){ 702 | $scope.session.token = $scope.token; 703 | $http({ 704 | url: '/dblogin', 705 | method: 'POST', 706 | data: $httpParamSerializerJQLike($scope.session), 707 | headers: { 708 | 'Content-Type': 'application/x-www-form-urlencoded' 709 | } 710 | }).success(function(data) { 711 | if (data.result.success == '1') { 712 | $scope.isDoubanLogin = true; 713 | $rootScope.$broadcast('isdoubanlogin:update', true); 714 | Notification.success("登录豆瓣成功"); 715 | } 716 | else { 717 | $scope.validcode_url = data.result.path; 718 | $scope.token = data.result.token; 719 | } 720 | }); 721 | $scope.session.solution = ""; 722 | }; 723 | 724 | $scope.logoutDouban = function() { 725 | $http({ 726 | url: '/dblogout', 727 | method: 'GET' 728 | }).success(function(data) { 729 | $scope.isDoubanLogin = false; 730 | $rootScope.$broadcast('isdoubanlogin:update', false); 731 | Notification.success("退出登录豆瓣成功"); 732 | $scope.getLoginInfo(); 733 | }); 734 | }; 735 | 736 | $scope.importDoubanFav = function(){ 737 | $http({ 738 | url: '/dbfav', 739 | method: 'POST', 740 | data: $httpParamSerializerJQLike({command:'start'}), 741 | headers: { 742 | 'Content-Type': 'application/x-www-form-urlencoded' 743 | } 744 | }).success(function(data) { 745 | $scope.status = '正在进行中:' + data.result.progress + '%'; 746 | $scope.start(); 747 | }); 748 | }; 749 | 750 | var promise; 751 | 752 | $scope.start = function() { 753 | // stops any running interval to avoid two intervals running at the same time 754 | $scope.stop(); 755 | 756 | // store the interval promise 757 | promise = $interval(poll, 1000); 758 | }; 759 | 760 | $scope.stop = function() { 761 | $interval.cancel(promise); 762 | }; 763 | $scope.$on('$destroy', function() { 764 | $scope.stop(); 765 | }); 766 | 767 | function poll(){ 768 | $http({ 769 | url: '/dbfav', 770 | method: 'POST', 771 | data: $httpParamSerializerJQLike({command:'status'}), 772 | headers: { 773 | 'Content-Type': 'application/x-www-form-urlencoded' 774 | } 775 | }).success(function(data) { 776 | $scope.status = '正在进行中:' + data.result.progress + '%'; 777 | if (data.result.progress == 100) { 778 | $scope.stop(); 779 | $scope.status = ''; 780 | Notification.success("红心兆赫已导入我的歌单"); 781 | } 782 | }); 783 | } 784 | }]); 785 | 786 | })(); -------------------------------------------------------------------------------- /listen1/media/js/vendor/angular-ui-notification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating 3 | * @author Alex_Crack 4 | * @version v0.1.0 5 | * @link https://github.com/alexcrack/angular-ui-notification 6 | * @license MIT 7 | */ 8 | angular.module('ui-notification',[]); 9 | 10 | angular.module('ui-notification').provider('Notification', function() { 11 | 12 | this.options = { 13 | delay: 5000, 14 | startTop: 10, 15 | startRight: 10, 16 | verticalSpacing: 10, 17 | horizontalSpacing: 10, 18 | positionX: 'right', 19 | positionY: 'top', 20 | replaceMessage: false, 21 | templateUrl: 'angular-ui-notification.html' 22 | }; 23 | 24 | this.setOptions = function(options) { 25 | if (!angular.isObject(options)) throw new Error("Options should be an object!"); 26 | this.options = angular.extend({}, this.options, options); 27 | }; 28 | 29 | this.$get = ["$timeout", "$http", "$compile", "$templateCache", "$rootScope", "$injector", "$sce", "$q", "$window", function($timeout, $http, $compile, $templateCache, $rootScope, $injector, $sce, $q, $window) { 30 | var options = this.options; 31 | 32 | var startTop = options.startTop; 33 | var startRight = options.startRight; 34 | var verticalSpacing = options.verticalSpacing; 35 | var horizontalSpacing = options.horizontalSpacing; 36 | var delay = options.delay; 37 | 38 | var messageElements = []; 39 | var isResizeBound = false; 40 | 41 | var notify = function(args, t){ 42 | var deferred = $q.defer(); 43 | 44 | if (typeof args !== 'object') { 45 | args = {message:args}; 46 | } 47 | 48 | args.scope = args.scope ? args.scope : $rootScope; 49 | args.template = args.templateUrl ? args.templateUrl : options.templateUrl; 50 | args.delay = !angular.isUndefined(args.delay) ? args.delay : delay; 51 | args.type = t || options.type || ''; 52 | args.positionY = args.positionY ? args.positionY : options.positionY; 53 | args.positionX = args.positionX ? args.positionX : options.positionX; 54 | args.replaceMessage = args.replaceMessage ? args.replaceMessage : options.replaceMessage; 55 | 56 | $http.get(args.template,{cache: $templateCache}).success(function(template) { 57 | 58 | var scope = args.scope.$new(); 59 | scope.message = $sce.trustAsHtml(args.message); 60 | scope.title = $sce.trustAsHtml(args.title); 61 | scope.t = args.type.substr(0,1); 62 | scope.delay = args.delay; 63 | 64 | var reposite = function() { 65 | var j = 0; 66 | var k = 0; 67 | var lastTop = startTop; 68 | var lastRight = startRight; 69 | var lastPosition = []; 70 | for(var i = messageElements.length - 1; i >= 0; i --) { 71 | var element = messageElements[i]; 72 | if (args.replaceMessage && i < messageElements.length - 1) { 73 | element.addClass('killed'); 74 | continue; 75 | } 76 | var elHeight = parseInt(element[0].offsetHeight); 77 | var elWidth = parseInt(element[0].offsetWidth); 78 | var position = lastPosition[element._positionY+element._positionX]; 79 | 80 | if ((top + elHeight) > window.innerHeight) { 81 | position = startTop; 82 | k ++; 83 | j = 0; 84 | } 85 | 86 | var top = (lastTop = position ? (j === 0 ? position : position + verticalSpacing) : startTop); 87 | var right = lastRight + (k * (horizontalSpacing + elWidth)); 88 | 89 | element.css(element._positionY, top + 'px'); 90 | if (element._positionX == 'center') { 91 | element.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px'); 92 | } else { 93 | element.css(element._positionX, right + 'px'); 94 | } 95 | 96 | lastPosition[element._positionY+element._positionX] = top + elHeight; 97 | 98 | j ++; 99 | } 100 | }; 101 | 102 | var templateElement = $compile(template)(scope); 103 | templateElement._positionY = args.positionY; 104 | templateElement._positionX = args.positionX; 105 | templateElement.addClass(args.type); 106 | templateElement.bind('webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd click', function(e){ 107 | e = e.originalEvent || e; 108 | if (e.type === 'click' || (e.propertyName === 'opacity' && e.elapsedTime >= 1)){ 109 | templateElement.remove(); 110 | messageElements.splice(messageElements.indexOf(templateElement), 1); 111 | scope.$destroy(); 112 | reposite(); 113 | } 114 | }); 115 | if (angular.isNumber(args.delay)) { 116 | $timeout(function() { 117 | templateElement.addClass('killed'); 118 | }, args.delay); 119 | } 120 | 121 | angular.element(document.getElementsByTagName('body')).append(templateElement); 122 | var offset = -(parseInt(templateElement[0].offsetHeight) + 50); 123 | templateElement.css(templateElement._positionY, offset + "px"); 124 | messageElements.push(templateElement); 125 | 126 | scope._templateElement = templateElement; 127 | 128 | scope.kill = function(isHard) { 129 | if (isHard) { 130 | messageElements.splice(messageElements.indexOf(scope._templateElement), 1); 131 | scope._templateElement.remove(); 132 | scope.$destroy(); 133 | $timeout(reposite); 134 | } else { 135 | scope._templateElement.addClass('killed'); 136 | } 137 | }; 138 | 139 | $timeout(reposite); 140 | 141 | if (!isResizeBound) { 142 | angular.element($window).bind('resize', function(e) { 143 | $timeout(reposite); 144 | }); 145 | isResizeBound = true; 146 | } 147 | 148 | deferred.resolve(scope); 149 | 150 | }).error(function(data){ 151 | throw new Error('Template ('+args.template+') could not be loaded. ' + data); 152 | }); 153 | 154 | return deferred.promise; 155 | }; 156 | 157 | notify.primary = function(args) { 158 | return this(args, 'primary'); 159 | }; 160 | notify.error = function(args) { 161 | return this(args, 'error'); 162 | }; 163 | notify.success = function(args) { 164 | return this(args, 'success'); 165 | }; 166 | notify.info = function(args) { 167 | return this(args, 'info'); 168 | }; 169 | notify.warning = function(args) { 170 | return this(args, 'warning'); 171 | }; 172 | 173 | notify.clearAll = function() { 174 | angular.forEach(messageElements, function(element) { 175 | element.addClass('killed'); 176 | }); 177 | }; 178 | 179 | return notify; 180 | }]; 181 | }); 182 | 183 | angular.module("ui-notification").run(["$templateCache", function($templateCache) {$templateCache.put("angular-ui-notification.html","

");}]); 184 | -------------------------------------------------------------------------------- /listen1/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/models/__init__.py -------------------------------------------------------------------------------- /listen1/models/playlist.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import json 3 | import logging 4 | 5 | from settings import MEDIA_ROOT 6 | 7 | logger = logging.getLogger('listenone.' + __name__) 8 | 9 | 10 | class Playlist(object): 11 | def __init__(self): 12 | self.cover_img_url = '' 13 | self.title = '' 14 | self.play_count = 0 15 | self.list_id = '' 16 | 17 | manager_instance = None 18 | 19 | 20 | class PlaylistManager(object): 21 | 22 | default_path = MEDIA_ROOT + '/user/' + 'playlist.json' 23 | 24 | @classmethod 25 | def shared_instance(cls): 26 | global manager_instance 27 | if manager_instance is None: 28 | manager_instance = PlaylistManager() 29 | return manager_instance 30 | 31 | def __init__(self, path=None): 32 | self.path = self.default_path if path is None else path 33 | try: 34 | self.load_from_disk() 35 | self.nextid = 1 36 | for l in self.mylists: 37 | listid = int(l['id'].split('_')[1]) 38 | if listid >= self.nextid: 39 | self.nextid = listid + 1 40 | except: 41 | self.mylists = [] 42 | self.nextid = 1 43 | 44 | def save_to_disk(self): 45 | s = json.dumps(self.mylists) 46 | with open(self.path, 'w') as f: 47 | f.write(s) 48 | 49 | def load_from_disk(self): 50 | with open(self.path, 'r') as f: 51 | s = f.read() 52 | self.mylists = json.loads(s) 53 | 54 | def get_playlist(self, list_id): 55 | targetlist = None 56 | for playlist in self.mylists: 57 | if playlist['id'] == list_id: 58 | targetlist = playlist 59 | break 60 | return targetlist 61 | 62 | def create_playlist( 63 | self, title, cover_img_url='/static/images/mycover.jpg', 64 | tracks=None): 65 | newlist_id = 'my_' + str(self.nextid) 66 | if tracks is None: 67 | my_tracks = [] 68 | else: 69 | my_tracks = tracks 70 | newlist = dict( 71 | title=title, 72 | id=newlist_id, 73 | cover_img_url=cover_img_url, 74 | tracks=my_tracks) 75 | self.nextid += 1 76 | self.mylists.append(newlist) 77 | self.save_to_disk() 78 | return newlist_id 79 | 80 | def list_playlist(self): 81 | resultlist = [] 82 | for l in self.mylists: 83 | r = dict( 84 | cover_img_url=l['cover_img_url'], 85 | title=l['title'], 86 | play_count=0, 87 | list_id=l['id'],) 88 | resultlist.append(r) 89 | return resultlist 90 | 91 | def remove_playlist(self, list_id): 92 | target_index = -1 93 | for index, playlist in enumerate(self.mylists): 94 | if playlist['id'] == list_id: 95 | target_index = index 96 | break 97 | self.mylists = self.mylists[:target_index] + \ 98 | self.mylists[target_index + 1:] 99 | self.save_to_disk() 100 | 101 | def is_exist_in_playlist(self, track_id, list_id): 102 | playlist = self.get_playlist(list_id) 103 | for d in playlist['tracks']: 104 | if d['id'] == track_id: 105 | return True 106 | return False 107 | 108 | def add_track_in_playlist(self, track, list_id): 109 | track_id = track['id'] 110 | if self.is_exist_in_playlist(track_id, list_id): 111 | return 112 | playlist = self.get_playlist(list_id) 113 | playlist['tracks'].append(track) 114 | self.save_to_disk() 115 | 116 | def remove_track_in_playlist(self, track_id, list_id): 117 | playlist = self.get_playlist(list_id) 118 | target_index = -1 119 | for index, d in enumerate(playlist['tracks']): 120 | if d['id'] == track_id: 121 | target_index = index 122 | break 123 | playlist['tracks'] = playlist['tracks'][:target_index] + \ 124 | playlist['tracks'][target_index + 1:] 125 | -------------------------------------------------------------------------------- /listen1/replay/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import netease 3 | import qq 4 | import xiami 5 | import douban 6 | 7 | 8 | def get_provider_by_name(source): 9 | if source == 'netease': 10 | return netease 11 | if source == 'xiami': 12 | return xiami 13 | if source == 'qq': 14 | return qq 15 | if source == 'douban': 16 | return douban 17 | 18 | 19 | def get_provider(item_id): 20 | provider_item = item_id.split('_')[0] 21 | if provider_item.startswith('ne'): 22 | return netease 23 | if provider_item.startswith('xm'): 24 | return xiami 25 | if provider_item.startswith('qq'): 26 | return qq 27 | if provider_item.startswith('db'): 28 | return douban 29 | 30 | 31 | def get_provider_list(): 32 | return [netease, xiami, qq, douban] 33 | -------------------------------------------------------------------------------- /listen1/replay/douban.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | douban.fm music provider. 4 | ''' 5 | import logging 6 | import json 7 | import os.path 8 | import re 9 | import urllib 10 | import HTMLParser 11 | 12 | from replay import h 13 | from settings import MEDIA_ROOT 14 | 15 | 16 | logger = logging.getLogger('listenone.' + __name__) 17 | 18 | 19 | def filetype(): 20 | return '.mp3' 21 | 22 | 23 | def _db_h(url, v=None, auth=False): 24 | if auth: 25 | token, ck = get_douban_token_ck() 26 | if token is None or ck is None: 27 | return None 28 | cookie = 'dbcl2="' + token + '"; fmNlogin="y";' + \ 29 | ' ck="' + ck + '";' 30 | else: 31 | cookie = 'ac="1460193849";' 32 | # http request 33 | extra_headers = { 34 | 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*', 35 | 'Accept-Encoding': 'gzip,deflate,sdch', 36 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 37 | 'Connection': 'keep-alive', 38 | 'Content-Type': 'application/x-www-form-urlencoded', 39 | 'Host': 'douban.fm', 40 | 'Referer': 'http://douban.fm/search', 41 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2)' + 42 | ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome' + 43 | '/33.0.1750.152 Safari/537.36', 44 | 'Cookie': cookie, 45 | } 46 | return h(url, v=v, extra_headers=extra_headers) 47 | 48 | 49 | def get_douban_token_file_path(): 50 | root_dir = MEDIA_ROOT + '/user/' 51 | filename = root_dir + 'douban_userinfo.json' 52 | return filename 53 | 54 | 55 | def get_douban_token_ck(): 56 | path = get_douban_token_file_path() 57 | if not os.path.isfile(path): 58 | return None, None 59 | with open(path, 'r') as f: 60 | content = f.read() 61 | d = json.loads(content) 62 | return d['token'], d['ck'] 63 | 64 | 65 | def set_douban_token_ck(token, ck): 66 | filename = get_douban_token_file_path() 67 | with open(filename, 'w') as f: 68 | f.write(json.dumps(dict(token=token, ck=ck))) 69 | 70 | 71 | def remove_douban_token_ck(): 72 | try: 73 | os.remove(get_douban_token_file_path()) 74 | except: 75 | pass 76 | 77 | 78 | def _gen_url_params(d): 79 | for k, v in d.iteritems(): 80 | d[k] = unicode(v).encode('utf-8') 81 | return urllib.urlencode(d) 82 | 83 | 84 | def _convert_song(song): 85 | album_title = song.get('album_title') 86 | if album_title is None: 87 | album_title = '' 88 | d = { 89 | 'id': 'dbtrack_' + str(song['id']), 90 | 'title': song['title'], 91 | 'artist': song['artist_name'], 92 | 'artist_id': 'dbartist_' + '0', 93 | 'album': album_title, 94 | 'album_id': 'dbalbum_' + '0', 95 | 'img_url': song['cover'], 96 | 'url': song['url'], 97 | 'source': 'douban', 98 | 'source_url': 'https://music.douban.com/search?q=' + 99 | album_title + '&sid=' + str(song['id']), 100 | } 101 | params = _gen_url_params(d) 102 | d['url'] = '/track_file?' + params 103 | return d 104 | 105 | 106 | def _convert_song2(song): 107 | album_title = song.get('albumtitle') 108 | if album_title is None: 109 | album_title = '' 110 | d = { 111 | 'id': 'dbtrack_' + str(song['sid']), 112 | 'title': song['title'], 113 | 'artist': song['singers'][0]['name'], 114 | 'artist_id': 'dbartist_' + song['singers'][0]['id'], 115 | 'album': album_title, 116 | 'album_id': 'dbalbum_' + song['aid'], 117 | 'img_url': song['picture'], 118 | 'url': song['url'], 119 | 'source': 'douban', 120 | 'source_url': 'https://music.douban.com/subject/%s/' % song['aid'], 121 | } 122 | params = _gen_url_params(d) 123 | d['url'] = '/track_file?' + params 124 | return d 125 | 126 | 127 | def _top_playlists(category=1, order='hot', offset=0, limit=60): 128 | action = 'http://douban.fm/j/v2/songlist/explore?type=hot&genre=1&' + \ 129 | 'limit=60&sample_cnt=5' 130 | data = json.loads(_db_h(action, auth=True)) 131 | return data 132 | 133 | 134 | # -------------standard interface part------------------ 135 | 136 | 137 | def get_captcha_token(path): 138 | human_url = 'http://douban.fm/j/new_captcha' 139 | captcha_token = h(human_url)[1:-1] 140 | pic_url = 'http://douban.fm/misc/captcha?size=m&id=' + captcha_token 141 | c = h(pic_url) 142 | with open(path, 'wb') as f: 143 | f.write(c) 144 | return captcha_token 145 | 146 | 147 | def login(user, password, token, solution): 148 | login_url = 'http://douban.fm/j/login' 149 | v = dict( 150 | source='radio', alias=user, form_password=password, 151 | captcha_id=token, captcha_solution=solution, task="sync_channel_list") 152 | cookie = r'openExpPan=Y; flag="ok"; ac="1448675235"; bid="2dLYThA' + \ 153 | 'DnhQ";_pk_ref.100002.6447=%5B%22%22%2C%22%22%2C1448714740%2C' + \ 154 | '%22http%3A%2F%2Fwww.douban.com%2F%22%5D; _ga=GA1.2.16347733' + \ 155 | '49.1402330632; _pk_id.100002.6447=7d9729e0b4385d49.14023306' + \ 156 | '33.88.1448714753.1448700119.; _pk_ses.100002.6447=*; dbcl2="' + \ 157 | token + '"; fmNlogin="y"; ck="boPw"; _gat=1' 158 | 159 | user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) ' + \ 160 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490' + \ 161 | '.86 Safari/537.36' 162 | referer = 'http://douban.fm/' 163 | xwith = 'ShockwaveFlash/19.0.0.245' 164 | xwith = 'XMLHttpRequest' 165 | headers = { 166 | 'User-Agent': user_agent, 167 | 'Cookie': cookie, 168 | 'Referer': referer, 169 | 'X-Requested-With': xwith, 170 | 'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2', 171 | 'Accept-Encoding': 'gzip, deflate, sdch', 172 | } 173 | 174 | def post_handler(response, result): 175 | try: 176 | ck = json.loads(result)['user_info']['ck'] 177 | except: 178 | ck = None 179 | cookie = response.info().getheader('Set-Cookie') 180 | if cookie and ck is not None: 181 | r = re.findall(r'dbcl2="([^"]+)"', cookie) 182 | if r: 183 | token = r[0] 184 | return token + '|' + ck 185 | else: 186 | return None 187 | 188 | return h( 189 | login_url, v, progress=False, extra_headers=headers, 190 | post_handler=post_handler, return_post=True) 191 | 192 | 193 | def list_playlist(): 194 | h = HTMLParser.HTMLParser() 195 | result = [] 196 | for l in _top_playlists(): 197 | d = dict( 198 | cover_img_url=l['cover'], 199 | title=h.unescape(l['title']), 200 | play_count=0, 201 | list_id='dbplaylist_' + str(l['id']), 202 | ) 203 | result.append(d) 204 | return result 205 | 206 | 207 | def get_playlist(playlist_id): 208 | url = 'http://douban.fm/j/v2/songlist/%s/?kbps=192' % playlist_id 209 | data = json.loads(_db_h(url, auth=True)) 210 | info = dict( 211 | cover_img_url=data['cover'], 212 | title=data['title'], 213 | id='dbplaylist_' + playlist_id) 214 | result = [] 215 | for song in data['songs']: 216 | result.append(_convert_song2(song)) 217 | return dict(tracks=result, info=info) 218 | 219 | 220 | def get_artist(artist_id): 221 | url = 'http://douban.fm/j/v2/artist/%s/' % str(artist_id) 222 | data = json.loads(_db_h(url, auth=True)) 223 | info = dict( 224 | cover_img_url=data['avatar'], 225 | title=data['name_usual'], 226 | id='dbartist_' + artist_id) 227 | result = [] 228 | for song in data['songlist']['songs']: 229 | result.append(_convert_song2(song)) 230 | return dict(tracks=result, info=info) 231 | 232 | 233 | def search_track(keyword): 234 | keyword = keyword.encode("utf8") 235 | search_url = 'http://douban.fm/j/v2/query/all?q=%s&start=0&limit=100' \ 236 | % keyword 237 | data = json.loads(_db_h(search_url)) 238 | result = [] 239 | for song in data[1]["items"]: 240 | if not song['playable']: 241 | continue 242 | result.append(_convert_song(song)) 243 | return result 244 | -------------------------------------------------------------------------------- /listen1/replay/netease.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | netease music provider. 4 | ''' 5 | import base64 6 | import hashlib 7 | import json 8 | import logging 9 | import os.path 10 | import urllib 11 | import urllib2 12 | 13 | import pyaes 14 | 15 | from replay import h 16 | 17 | 18 | logger = logging.getLogger('listenone.' + __name__) 19 | 20 | 21 | def filetype(): 22 | return '.mp3' 23 | 24 | 25 | # 歌曲加密算法, 基于https://github.com/yanunon/NeteaseCloudMusic脚本实现 26 | def _encrypted_id(id): 27 | magic = bytearray('3go8&$8*3*3h0k(2)2') 28 | song_id = bytearray(id) 29 | magic_len = len(magic) 30 | for i in xrange(len(song_id)): 31 | song_id[i] = song_id[i] ^ magic[i % magic_len] 32 | m = hashlib.md5(song_id) 33 | result = m.digest().encode('base64')[:-1] 34 | result = result.replace('/', '_') 35 | result = result.replace('+', '-') 36 | return result 37 | 38 | 39 | modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b72' + \ 40 | '5152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbd' + \ 41 | 'a92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe48' + \ 42 | '75d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 43 | nonce = '0CoJUm6Qyw8W8jud' 44 | pubKey = '010001' 45 | 46 | 47 | def _create_secret_key(size): 48 | randlist = map(lambda xx: (hex(ord(xx))[2:]), os.urandom(size)) 49 | return (''.join(randlist))[0:16] 50 | 51 | 52 | def _aes_encrypt(text, sec_key): 53 | pad = 16 - len(text) % 16 54 | text = text + pad * chr(pad) 55 | aes = pyaes.AESModeOfOperationCBC(sec_key, iv='0102030405060708') 56 | ciphertext = '' 57 | while text != '': 58 | ciphertext += aes.encrypt(text[:16]) 59 | text = text[16:] 60 | ciphertext = base64.b64encode(ciphertext) 61 | return ciphertext 62 | 63 | 64 | def _rsa_encrypt(text, pub_key, modulus): 65 | text = text[::-1] 66 | rs = int(text.encode('hex'), 16) ** int(pubKey, 16) % int(modulus, 16) 67 | return format(rs, 'x').zfill(256) 68 | 69 | 70 | def _encrypted_request(text): 71 | text = json.dumps(text) 72 | sec_key = _create_secret_key(16) 73 | enc_text = _aes_encrypt(_aes_encrypt(text, nonce), sec_key) 74 | enc_sec_key = _rsa_encrypt(sec_key, pubKey, modulus) 75 | data = { 76 | 'params': enc_text, 77 | 'encSecKey': enc_sec_key 78 | } 79 | return data 80 | 81 | 82 | # 参考 https://github.com/darknessomi/musicbox 83 | # 歌单(网友精选碟) hot||new http://music.163.com/#/discover/playlist/ 84 | def _top_playlists(category=u'全部', order='hot', offset=0, limit=60): 85 | category = urllib2.quote(category.encode("utf8")) 86 | action = 'http://music.163.com/api/playlist/list?cat=' + category + \ 87 | '&order=' + order + '&offset=' + str(offset) + \ 88 | '&total=' + ('true' if offset else 'false') + '&limit=' + str(limit) 89 | data = json.loads(_ne_h(action)) 90 | return data['playlists'] 91 | 92 | 93 | def _ne_h(url, v=None): 94 | # http request 95 | extra_headers = { 96 | 'Accept': '*/*', 97 | 'Accept-Encoding': 'gzip,deflate,sdch', 98 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 99 | 'Connection': 'keep-alive', 100 | 'Content-Type': 'application/x-www-form-urlencoded', 101 | 'Host': 'music.163.com', 102 | 'Referer': 'http://music.163.com/search/', 103 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2)' + 104 | ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome' + 105 | '/33.0.1750.152 Safari/537.36' 106 | } 107 | return h(url, v=v, extra_headers=extra_headers) 108 | 109 | 110 | def _gen_url_params(d): 111 | for k, v in d.iteritems(): 112 | d[k] = unicode(v).encode('utf-8') 113 | return urllib.urlencode(d) 114 | 115 | 116 | def _convert_song(song): 117 | d = { 118 | 'id': 'netrack_' + str(song['id']), 119 | 'title': song['name'], 120 | 'artist': song['artists'][0]['name'], 121 | 'artist_id': 'neartist_' + str(song['artists'][0]['id']), 122 | 'album': song['album']['name'], 123 | 'album_id': 'nealbum_' + str(song['album']['id']), 124 | 'source': 'netease', 125 | 'source_url': 'http://music.163.com/#/song?id=' + str(song['id']), 126 | } 127 | if 'picUrl' in song['album']: 128 | d['img_url'] = song['album']['picUrl'] 129 | else: 130 | d['img_url'] = '' 131 | params = _gen_url_params(d) 132 | d['url'] = '/track_file?' + params 133 | return d 134 | 135 | # -------------standard interface part------------------ 136 | 137 | 138 | # https://github.com/darknessomi/musicbox/wiki/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E6%96%B0%E7%89%88WebAPI%E5%88%86%E6%9E%90%E3%80%82 139 | def get_url_by_id(song_id): 140 | csrf = '' 141 | d = { 142 | "ids": [song_id], 143 | "br": 12800, 144 | "csrf_token": csrf 145 | } 146 | url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token=' 147 | request = _encrypted_request(d) 148 | response = json.loads(_ne_h(url, request)) 149 | return response['data'][0]['url'] 150 | 151 | 152 | def list_playlist(): 153 | result = [] 154 | for l in _top_playlists(): 155 | d = dict( 156 | cover_img_url=l['coverImgUrl'], 157 | title=l['name'], 158 | play_count=l['playCount'], 159 | list_id='neplaylist_' + str(l['id']),) 160 | result.append(d) 161 | return result 162 | 163 | 164 | def get_playlist(playlist_id): 165 | url = 'http://music.163.com/api/playlist/detail?id=' + playlist_id 166 | data = json.loads(_ne_h(url)) 167 | info = dict( 168 | cover_img_url=data['result']['coverImgUrl'], 169 | title=data['result']['name'], 170 | id='neplaylist_' + playlist_id) 171 | result = [] 172 | for song in data['result']['tracks']: 173 | if song['status'] == -1: 174 | continue 175 | result.append(_convert_song(song)) 176 | return dict(tracks=result, info=info) 177 | 178 | 179 | def get_artist(artist_id): 180 | url = 'http://music.163.com/api/artist/' + str(artist_id) 181 | data = json.loads(_ne_h(url)) 182 | info = dict( 183 | cover_img_url=data['artist']['picUrl'], 184 | title=data['artist']['name'], 185 | id='neartist_' + artist_id) 186 | result = [] 187 | for song in data['hotSongs']: 188 | if song['status'] == -1: 189 | continue 190 | result.append(_convert_song(song)) 191 | return dict(tracks=result, info=info) 192 | 193 | 194 | def get_artist_albums(artist_id): 195 | url = 'http://music.163.com/api/artist/albums/' + str(artist_id) + \ 196 | '?offset=0&limit=50' 197 | try: 198 | data = json.loads(_ne_h(url)) 199 | return data['hotAlbums'] 200 | except: 201 | return [] 202 | 203 | 204 | def get_album(album_id): 205 | url = 'http://music.163.com/api/album/%s/' % album_id 206 | data = json.loads(_ne_h(url)) 207 | info = dict( 208 | cover_img_url=data['album']['picUrl'], 209 | title=data['album']['name'], 210 | id='nealbum_' + str(album_id)) 211 | 212 | result = [] 213 | for song in data['album']['songs']: 214 | if song['status'] == -1: 215 | continue 216 | result.append(_convert_song(song)) 217 | return dict(tracks=result, info=info) 218 | 219 | 220 | def search_track(keyword): 221 | # return matched qq music songs 222 | keyword = keyword.encode("utf8") 223 | search_url = 'http://music.163.com/api/search/get' 224 | stype = 1 225 | offset = 0 226 | total = 'true' 227 | data = { 228 | 's': keyword, 229 | 'type': stype, 230 | 'offset': offset, 231 | 'total': total, 232 | 'limit': 60 233 | } 234 | jc = _ne_h(search_url, data) 235 | result = [] 236 | for song in json.loads(jc)["result"]["songs"]: 237 | if song['status'] == -1: 238 | continue 239 | result.append(_convert_song(song)) 240 | return result 241 | -------------------------------------------------------------------------------- /listen1/replay/qq.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | qq music provider, 4 | ''' 5 | import HTMLParser 6 | import json 7 | import logging 8 | import urllib 9 | import urllib2 10 | 11 | from replay import h 12 | 13 | 14 | logger = logging.getLogger('listenone.' + __name__) 15 | 16 | 17 | def filetype(): 18 | return '.m4a' 19 | 20 | 21 | def _qq_h(url, v=None): 22 | ''' 23 | http request 24 | ''' 25 | extra_headers = { 26 | 'Accept': '*/*', 27 | 'Accept-Encoding': 'gzip,deflate,sdch', 28 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 29 | 'Connection': 'keep-alive', 30 | 'Content-Type': 'application/x-www-form-urlencoded', 31 | 'Host': 'i.y.qq.com', 32 | 'Referer': 'http://y.qq.com/y/static/taoge/taoge_list.html' + 33 | '?pgv_ref=qqmusic.y.topmenu', 34 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2)' + 35 | ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome' + 36 | '/33.0.1750.152 Safari/537.36' 37 | } 38 | return h(url, v=v, extra_headers=extra_headers) 39 | 40 | 41 | def _get_qqtoken(): 42 | token_url = 'http://base.music.qq.com/fcgi-bin/fcg_musicexpress.fcg?' + \ 43 | 'json=3&guid=780782017&g_tk=938407465&loginUin=0&hostUin=0&' + \ 44 | 'format=jsonp&inCharset=GB2312&outCharset=GB2312¬ice=0&' + \ 45 | 'platform=yqq&jsonpCallback=jsonCallback&needNewCode=0' 46 | jc = h(token_url)[len("jsonCallback("):-len(");")] 47 | return json.loads(jc)["key"] 48 | 49 | 50 | def _get_image_url(qqimgid, img_type): 51 | if img_type == 'artist': 52 | category = 'mid_singer_300' 53 | elif img_type == 'album': 54 | category = 'mid_album_300' 55 | else: 56 | return None 57 | url = 'http://imgcache.qq.com/music/photo/%s/%s/%s/%s.jpg' 58 | image_url = url % (category, qqimgid[-2], qqimgid[-1], qqimgid) 59 | return image_url 60 | 61 | 62 | def _gen_url_params(d): 63 | for k, v in d.iteritems(): 64 | d[k] = unicode(v).encode('utf-8') 65 | return urllib.urlencode(d) 66 | 67 | 68 | def _convert_song(song): 69 | d = { 70 | 'id': 'qqtrack_' + str(song['songmid']), 71 | 'title': song['songname'], 72 | 'artist': song['singer'][0]['name'], 73 | 'artist_id': 'qqartist_' + str(song['singer'][0]['mid']), 74 | 'album': song['albumname'], 75 | 'album_id': 'qqalbum_' + str(song['albummid']), 76 | 'img_url': _get_image_url(song['albummid'], img_type='album'), 77 | 'source': 'qq', 78 | 'source_url': 'http://y.qq.com/#type=song&mid=' + 79 | str(song['songmid']) + '&tpl=yqq_song_detail', 80 | } 81 | params = _gen_url_params(d) 82 | d['url'] = '/track_file?' + params 83 | return d 84 | 85 | 86 | # -------------standard interface part------------------ 87 | 88 | 89 | def list_playlist(): 90 | url = 'http://i.y.qq.com/s.plcloud/fcgi-bin/fcg_get_diss_by_tag' + \ 91 | '.fcg?categoryId=10000000&sortId=1&sin=0&ein=29&' + \ 92 | 'format=jsonp&g_tk=5381&loginUin=0&hostUin=0&' + \ 93 | 'format=jsonp&inCharset=GB2312&outCharset=utf-8' + \ 94 | '¬ice=0&platform=yqq&jsonpCallback=' + \ 95 | 'MusicJsonCallback&needNewCode=0' 96 | response = _qq_h(url) 97 | data = json.loads(response[len('MusicJsonCallback('):-len(')')]) 98 | parser = HTMLParser.HTMLParser() 99 | result = [] 100 | for l in data['data']['list']: 101 | d = dict( 102 | cover_img_url=l['imgurl'], 103 | title=parser.unescape(l['dissname']), 104 | play_count=l['listennum'], 105 | list_id='qqplaylist_' + str(l['dissid']),) 106 | result.append(d) 107 | return result 108 | 109 | 110 | def get_playlist(playlist_id): 111 | url = 'http://i.y.qq.com/qzone-music/fcg-bin/fcg_ucc_getcdinfo_' + \ 112 | 'byids_cp.fcg?type=1&json=1&utf8=1&onlysong=0&jsonpCallback=' + \ 113 | 'jsonCallback&nosign=1&disstid=%s&g_tk=5381&loginUin=0&hostUin=0' + \ 114 | '&format=jsonp&inCharset=GB2312&outCharset=utf-8¬ice=0' + \ 115 | '&platform=yqq&jsonpCallback=jsonCallback&needNewCode=0' 116 | response = _qq_h(url % playlist_id) 117 | data = json.loads(response[len('jsonCallback('):-len(')')]) 118 | info = dict( 119 | cover_img_url=data['cdlist'][0]['logo'], 120 | title=data['cdlist'][0]['dissname'], 121 | id='qqplaylist_' + playlist_id) 122 | result = [] 123 | for song in data['cdlist'][0]['songlist']: 124 | result.append(_convert_song(song)) 125 | return dict(tracks=result, info=info) 126 | 127 | 128 | def get_artist(artist_id): 129 | url = 'http://i.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg' + \ 130 | '?platform=h5page&order=listen&begin=0&num=50&singermid=' + \ 131 | '%s&g_tk=938407465&uin=0&format=jsonp&' + \ 132 | 'inCharset=utf-8&outCharset=utf-8¬ice=0&platform=' + \ 133 | 'h5&needNewCode=1&from=h5&_=1459960621777&' + \ 134 | 'jsonpCallback=ssonglist1459960621772' 135 | response = _qq_h(url % artist_id) 136 | data = json.loads(response[len(' ssonglist1459960621772('):-len(')')]) 137 | info = dict( 138 | cover_img_url=_get_image_url(artist_id, img_type='artist'), 139 | title=data['data']['singer_name'], 140 | id='qqartist_' + artist_id) 141 | result = [] 142 | for song in data['data']['list']: 143 | result.append(_convert_song(song['musicData'])) 144 | return dict(tracks=result, info=info) 145 | 146 | 147 | def get_album(album_id): 148 | url = 'http://i.y.qq.com/v8/fcg-bin/fcg_v8_album_info_cp.fcg' + \ 149 | '?platform=h5page&albummid=%s&g_tk=938407465' + \ 150 | '&uin=0&format=jsonp&inCharset=utf-8&outCharset=utf-8' + \ 151 | '¬ice=0&platform=h5&needNewCode=1&_=1459961045571' + \ 152 | '&jsonpCallback=asonglist1459961045566' 153 | response = _qq_h(url % album_id) 154 | data = json.loads(response[len(' asonglist1459961045566('):-len(')')]) 155 | info = dict( 156 | cover_img_url=_get_image_url(album_id, img_type='album'), 157 | title=data['data']['name'], 158 | id='qqalbum_' + str(album_id)) 159 | 160 | result = [] 161 | for song in data['data']['list']: 162 | result.append(_convert_song(song)) 163 | return dict(tracks=result, info=info) 164 | 165 | 166 | def get_url_by_id(qqsid): 167 | token = _get_qqtoken() 168 | url = 'http://cc.stream.qqmusic.qq.com/C200%s.m4a?vkey=%s' + \ 169 | '&fromtag=0&guid=780782017' 170 | song_url = url % (qqsid, token) 171 | return song_url 172 | 173 | 174 | def search_track(keyword): 175 | ''' 176 | return matched qq music songs 177 | ''' 178 | keyword = urllib2.quote(keyword.encode("utf8")) 179 | url = 'http://i.y.qq.com/s.music/fcgi-bin/search_for_qq_cp?' + \ 180 | 'g_tk=938407465&uin=0&format=jsonp&inCharset=utf-8' + \ 181 | '&outCharset=utf-8¬ice=0&platform=h5&needNewCode=1' + \ 182 | '&w=%s&zhidaqu=1&catZhida=1' + \ 183 | '&t=0&flag=1&ie=utf-8&sem=1&aggr=0&perpage=20&n=20&p=1' + \ 184 | '&remoteplace=txt.mqq.all&_=1459991037831&jsonpCallback=jsonp4' 185 | response = _qq_h(url % keyword) 186 | data = json.loads(response[len('jsonp4('):-len(')')]) 187 | 188 | result = [] 189 | for song in data["data"]["song"]["list"]: 190 | result.append(_convert_song(song)) 191 | return result 192 | -------------------------------------------------------------------------------- /listen1/replay/replay.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | Basic Network Library. 4 | ''' 5 | import logging 6 | import os.path 7 | import sys 8 | import urllib 9 | import urllib2 10 | 11 | import StringIO 12 | import gzip 13 | 14 | 15 | logger = logging.getLogger('listenone.' + __name__) 16 | 17 | 18 | ######################################## 19 | # network 20 | ######################################## 21 | def chunk_report(bytes_so_far, chunk_size, total_size): 22 | percent = float(bytes_so_far) / total_size 23 | percent = round(percent * 100, 2) 24 | sys.stdout.write( 25 | "Downloaded %d of %d bytes (%0.2f%%)\r" % 26 | (bytes_so_far, total_size, percent)) 27 | 28 | if bytes_so_far >= total_size: 29 | sys.stdout.write('\n') 30 | 31 | 32 | def chunk_read(response, chunk_size=8192, report_hook=None): 33 | total_size = response.info().getheader('Content-Length').strip() 34 | total_size = int(total_size) 35 | bytes_so_far = 0 36 | 37 | total = '' 38 | while 1: 39 | chunk = response.read(chunk_size) 40 | bytes_so_far += len(chunk) 41 | 42 | if not chunk: 43 | break 44 | total += chunk 45 | if report_hook: 46 | report_hook(bytes_so_far, chunk_size, total_size) 47 | return total 48 | 49 | 50 | def h( 51 | url, v=None, progress=False, extra_headers={}, 52 | post_handler=None, return_post=False): 53 | ''' 54 | base http request 55 | progress: show progress information 56 | need_auth: need douban account login 57 | ''' 58 | logger.debug('fetching url:' + url) 59 | user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) ' + \ 60 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86' + \ 61 | ' Safari/537.36' 62 | headers = {'User-Agent': user_agent} 63 | headers.update(extra_headers) 64 | 65 | data = urllib.urlencode(v) if v else None 66 | req = urllib2.Request(url, data, headers) 67 | response = urllib2.urlopen(req) 68 | if progress: 69 | result = chunk_read(response, report_hook=chunk_report) 70 | else: 71 | result = response.read() 72 | if response.info().get('Content-Encoding') == 'gzip': 73 | buf = StringIO.StringIO(result) 74 | f = gzip.GzipFile(fileobj=buf) 75 | result = f.read() 76 | if post_handler: 77 | post_result = post_handler(response, result) 78 | if return_post: 79 | return post_result 80 | return result 81 | 82 | 83 | def w(url, path, overwrite=False): 84 | ''' 85 | write file from url to path 86 | use_cache: use file if already exists 87 | ''' 88 | if os.path.isfile(path) and not overwrite: 89 | return 90 | c = h(url, progress=True) 91 | with open(path, 'wb') as f: 92 | f.write(c) 93 | -------------------------------------------------------------------------------- /listen1/replay/xiami.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | ''' 3 | xiami music provider. 4 | ''' 5 | import json 6 | import logging 7 | import urllib 8 | import urllib2 9 | 10 | from replay import h 11 | 12 | 13 | logger = logging.getLogger('listenone.' + __name__) 14 | 15 | 16 | # https://github.com/Flowerowl/xiami 17 | def caesar(location): 18 | num = int(location[0]) 19 | avg_len = int(len(location[1:]) / num) 20 | remainder = int(len(location[1:]) % num) 21 | result = [ 22 | location[i * (avg_len + 1) + 1: (i + 1) * (avg_len + 1) + 1] 23 | for i in range(remainder)] 24 | result.extend( 25 | [ 26 | location[(avg_len + 1) * remainder:] 27 | [i * avg_len + 1: (i + 1) * avg_len + 1] 28 | for i in range(num - remainder)]) 29 | url = urllib.unquote( 30 | ''.join([ 31 | ''.join([result[j][i] for j in range(num)]) 32 | for i in range(avg_len) 33 | ]) + 34 | ''.join([result[r][-1] for r in range(remainder)])).replace('^', '0') 35 | return url 36 | 37 | 38 | def filetype(): 39 | return '.mp3' 40 | 41 | 42 | def _xm_h(url, v=None): 43 | ''' 44 | http request 45 | ''' 46 | extra_headers = { 47 | 'Accept': '*/*', 48 | 'Accept-Encoding': 'gzip,deflate,sdch', 49 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 50 | 'Connection': 'keep-alive', 51 | 'Content-Type': 'application/x-www-form-urlencoded', 52 | 'Host': 'api.xiami.com', 53 | 'Referer': 'http://m.xiami.com/', 54 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2)' + 55 | ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome' + 56 | '/33.0.1750.152 Safari/537.36', 57 | } 58 | return h(url, v=v, extra_headers=extra_headers) 59 | 60 | 61 | def _gen_url_params(d): 62 | for k, v in d.iteritems(): 63 | d[k] = unicode(v).encode('utf-8') 64 | return urllib.urlencode(d) 65 | 66 | 67 | def _convert_song(song): 68 | d = { 69 | 'id': 'xmtrack_' + str(song['song_id']), 70 | 'title': song['song_name'], 71 | 'artist': song['artist_name'], 72 | 'artist_id': 'xmartist_' + str(song['artist_id']), 73 | 'album': song['album_name'], 74 | 'album_id': 'xmalbum_' + str(song['album_id']), 75 | 'source': 'xiami', 76 | 'source_url': 'http://www.xiami.com/song/' + str(song['song_id']), 77 | } 78 | if 'logo' in song: 79 | d['img_url'] = song['logo'] 80 | else: 81 | d['img_url'] = '' 82 | params = _gen_url_params(d) 83 | d['url'] = '/track_file?' + params 84 | return d 85 | 86 | 87 | def _retina_url(s): 88 | return s[:-6] + s[-4:] 89 | 90 | 91 | # -------------standard interface part------------------ 92 | 93 | 94 | def search_track(keyword): 95 | ''' 96 | return matched qq music songs 97 | ''' 98 | keyword = urllib2.quote(keyword.encode("utf8")) 99 | search_url = 'http://api.xiami.com/web?v=2.0&app_key=1&key=' + keyword \ 100 | + '&page=1&limit=50&_ksTS=1459930568781_153&callback=jsonp154' + \ 101 | '&r=search/songs' 102 | response = _xm_h(search_url) 103 | json_string = response[len('jsonp154('):-len(')')] 104 | data = json.loads(json_string) 105 | result = [] 106 | for song in data['data']["songs"]: 107 | result.append(_convert_song(song)) 108 | return result 109 | 110 | 111 | def list_playlist(): 112 | url = 'http://api.xiami.com/web?v=2.0&app_key=1&_ksTS=1459927525542_91' + \ 113 | '&page=1&limit=60&callback=jsonp92&r=collect/recommend' 114 | resonpse = _xm_h(url) 115 | data = json.loads(resonpse[len('jsonp92('):-len(')')]) 116 | result = [] 117 | for l in data['data']: 118 | d = dict( 119 | cover_img_url=l['logo'], 120 | title=l['collect_name'], 121 | play_count=0, 122 | list_id='xmplaylist_' + str(l['list_id']),) 123 | result.append(d) 124 | return result 125 | 126 | 127 | def get_playlist(playlist_id): 128 | url = 'http://api.xiami.com/web?v=2.0&app_key=1&id=%s' % playlist_id + \ 129 | '&_ksTS=1459928471147_121&callback=jsonp122&r=collect/detail' 130 | resonpse = _xm_h(url) 131 | data = json.loads(resonpse[len('jsonp122('):-len(')')]) 132 | 133 | info = dict( 134 | cover_img_url=_retina_url(data['data']['logo']), 135 | title=data['data']['collect_name'], 136 | id='xmplaylist_' + playlist_id) 137 | result = [] 138 | for song in data['data']['songs']: 139 | result.append(_convert_song(song)) 140 | return dict(tracks=result, info=info) 141 | 142 | 143 | def get_artist(artist_id): 144 | url = 'http://api.xiami.com/web?v=2.0&app_key=1&id=%s' % str(artist_id) + \ 145 | '&page=1&limit=20&_ksTS=1459931285956_216' + \ 146 | '&callback=jsonp217&r=artist/detail' 147 | resonpse = _xm_h(url) 148 | data = json.loads(resonpse[len('jsonp217('):-len(')')]) 149 | artist_name = data['data']['artist_name'] 150 | info = dict( 151 | cover_img_url=_retina_url(data['data']['logo']), 152 | title=artist_name, 153 | id='xmartist_' + artist_id) 154 | 155 | url = 'http://api.xiami.com/web?v=2.0&app_key=1&id=%s' % str(artist_id) + \ 156 | '&page=1&limit=20&_ksTS=1459931285956_216' + \ 157 | '&callback=jsonp217&r=artist/hot-songs' 158 | resonpse = _xm_h(url) 159 | data = json.loads(resonpse[len('jsonp217('):-len(')')]) 160 | result = [] 161 | for song in data['data']: 162 | d = { 163 | 'id': 'xmtrack_' + str(song['song_id']), 164 | 'title': song['song_name'], 165 | 'artist': artist_name, 166 | 'artist_id': 'xmartist_' + artist_id, 167 | 'album': '', 168 | 'album_id': '', 169 | 'img_url': '', 170 | 'source': 'xiami', 171 | 'source_url': 'http://www.xiami.com/song/' + str(song['song_id']), 172 | } 173 | params = _gen_url_params(d) 174 | d['url'] = '/track_file?' + params 175 | result.append(d) 176 | return dict(tracks=result, info=info) 177 | 178 | 179 | def get_album(album_id): 180 | url = 'http://api.xiami.com/web?v=2.0&app_key=1&id=%s' % str(album_id) + \ 181 | '&page=1&limit=20&_ksTS=1459931285956_216' + \ 182 | '&callback=jsonp217&r=album/detail' 183 | resonpse = _xm_h(url) 184 | data = json.loads(resonpse[len('jsonp217('):-len(')')]) 185 | artist_name = data['data']['artist_name'] 186 | info = dict( 187 | cover_img_url=_retina_url(data['data']['album_logo']), 188 | title=data['data']['album_name'], 189 | id='xmalbum_' + album_id) 190 | result = [] 191 | for song in data['data']['songs']: 192 | d = { 193 | 'id': 'xmtrack_' + str(song['song_id']), 194 | 'title': song['song_name'], 195 | 'artist': artist_name, 196 | 'artist_id': 'xmartist_' + str(song['artist_id']), 197 | 'album': song['album_name'], 198 | 'album_id': 'xmalbum_' + str(song['album_id']), 199 | 'img_url': song['album_logo'], 200 | 'source': 'xiami', 201 | 'source_url': 'http://www.xiami.com/song/' + str(song['song_id']), 202 | } 203 | params = _gen_url_params(d) 204 | d['url'] = '/track_file?' + params 205 | result.append(d) 206 | return dict(tracks=result, info=info) 207 | 208 | 209 | def get_url_by_id(song_id): 210 | url = 'http://www.xiami.com/song/playlist/id/%s' % song_id + \ 211 | '/object_name/default/object_id/0/cat/json' 212 | response = h(url) 213 | secret = json.loads(response)['data']['trackList'][0]['location'] 214 | url = caesar(secret) 215 | return url 216 | -------------------------------------------------------------------------------- /listen1/requirements/common.txt: -------------------------------------------------------------------------------- 1 | tornado 2 | pyaes 3 | -------------------------------------------------------------------------------- /listen1/requirements/dev.txt: -------------------------------------------------------------------------------- 1 | nose==0.11.3 2 | fabric==0.9.2 3 | tornado 4 | pyaes -------------------------------------------------------------------------------- /listen1/requirements/production.txt: -------------------------------------------------------------------------------- 1 | tornado 2 | pyaes -------------------------------------------------------------------------------- /listen1/res/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/res/icon.icns -------------------------------------------------------------------------------- /listen1/res/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/res/icon.ico -------------------------------------------------------------------------------- /listen1/res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/res/icon.png -------------------------------------------------------------------------------- /listen1/settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import tornado 3 | import tornado.template 4 | import os 5 | import sys 6 | from tornado.options import define, options 7 | 8 | import environment 9 | import logconfig 10 | 11 | # Make filepaths relative to settings. 12 | path = lambda root,*a: os.path.join(root, *a) 13 | ROOT = os.path.dirname(os.path.abspath(__file__)) 14 | 15 | define("port", default=8888, help="run on the given port", type=int) 16 | define("config", default=None, help="tornado config file") 17 | define("debug", default=False, help="debug mode") 18 | 19 | # ignore multifork 20 | args = None 21 | try: 22 | index = sys.argv.index('--multiprocessing-fork') 23 | args = sys.argv[:index] + sys.argv[index+2:] 24 | except: 25 | pass 26 | 27 | tornado.options.parse_command_line(args) 28 | 29 | MEDIA_ROOT = path(ROOT, 'media') 30 | TEMPLATE_ROOT = path(ROOT, 'templates') 31 | 32 | # Deployment Configuration 33 | 34 | class DeploymentType: 35 | PRODUCTION = "PRODUCTION" 36 | DEV = "DEV" 37 | SOLO = "SOLO" 38 | STAGING = "STAGING" 39 | dict = { 40 | SOLO: 1, 41 | PRODUCTION: 2, 42 | DEV: 3, 43 | STAGING: 4 44 | } 45 | 46 | if 'DEPLOYMENT_TYPE' in os.environ: 47 | DEPLOYMENT = os.environ['DEPLOYMENT_TYPE'].upper() 48 | else: 49 | DEPLOYMENT = DeploymentType.SOLO 50 | 51 | settings = {} 52 | settings['debug'] = DEPLOYMENT != DeploymentType.PRODUCTION or options.debug 53 | settings['static_path'] = MEDIA_ROOT 54 | settings['cookie_secret'] = "your-cookie-secret" 55 | settings['xsrf_cookies'] = False 56 | settings['template_loader'] = tornado.template.Loader(TEMPLATE_ROOT) 57 | 58 | SYSLOG_TAG = "listenone" 59 | SYSLOG_FACILITY = logging.handlers.SysLogHandler.LOG_LOCAL2 60 | 61 | # See PEP 391 and logconfig for formatting help. Each section of LOGGERS 62 | # will get merged into the corresponding section of log_settings.py. 63 | # Handlers and log levels are set up automatically based on LOG_LEVEL and DEBUG 64 | # unless you set them here. Messages will not propagate through a logger 65 | # unless propagate: True is set. 66 | LOGGERS = { 67 | 'loggers': { 68 | 'listenone': { 69 | 'handlers' : ['console'] 70 | }, 71 | 'tornado': { 72 | 'handlers': ['console'] 73 | }, 74 | 75 | }, 76 | } 77 | 78 | if settings['debug']: 79 | LOG_LEVEL = logging.DEBUG 80 | else: 81 | LOG_LEVEL = logging.INFO 82 | USE_SYSLOG = DEPLOYMENT != DeploymentType.SOLO 83 | 84 | logconfig.initialize_logging(SYSLOG_TAG, SYSLOG_FACILITY, LOGGERS, 85 | LOG_LEVEL, USE_SYSLOG) 86 | 87 | if options.config: 88 | tornado.options.parse_config_file(options.config) 89 | -------------------------------------------------------------------------------- /listen1/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tornado Boilerplate 4 | 5 | 6 | 7 |

It worked!

8 | 9 | 10 | -------------------------------------------------------------------------------- /listen1/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Listen 1 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
{{! dialog_title }} ×
34 |
35 | 36 | 37 |
    38 |
  • 39 | 40 |

    新建歌单

    41 |
  • 42 |
  • 43 | 44 |

    {{! playlist.title }}

    45 |
  • 46 |
47 | 48 | 49 |
50 | 51 | 52 | 53 |
54 | 55 | 56 |
57 | 68 | 69 | 70 |

{{! status }}

71 |
72 | 73 |
74 |
75 | 76 | 77 |
78 |
79 | 80 |

Listen 1

81 | 89 |
90 |
91 | 92 | 93 | 94 |
95 |
96 |
97 |
98 |
    99 | 100 |
  • 101 |
    102 | 103 | 104 |
    105 | 106 | 107 | 108 |
    109 |
    110 |

    111 | {{!i.title}} 112 |

    113 |
  • 114 |
115 |
116 |
117 |
118 |
119 | 120 | 121 | 122 |
123 |
124 |
125 |
126 | 127 | 128 | 129 | 130 |
131 |
132 |
    133 |
  • 134 |
    135 | 136 | 137 |
    138 | 139 |
    140 |
    141 |

    142 | {{!i.title}} 143 |

    144 |
  • 145 |
146 |
147 |
148 |
149 |
150 | 151 | 152 | 153 |
154 |
155 |
156 | 157 | 181 |
182 |
183 |
184 | 185 | 186 | 187 |
188 |
189 |
190 |
第三方登录
191 |
192 |
豆瓣:
193 |
豆瓣:
194 |
195 |
关于
196 |
197 |

Listen 1 主页: https://github.com/listen1/listen1

198 |

Listen 1 邮箱: githublisten1@gmail.com

199 |

当前版本 1.0 (本软件基于MIT协议开源免费)

200 |
201 |
202 |
203 |
204 | 205 | 206 | 207 |
208 |
209 |
210 | 211 |
212 |
213 | × 214 |
215 | 216 |
217 |
218 | 219 |
220 |
221 |

{{! playlist_title }}

222 | 播放 223 | 删除 224 | 收藏 225 |
226 |
227 | 240 |
241 |
242 |
243 |
244 | 245 | 246 | 247 |
248 |
249 |
250 | 251 | 播放/暂停 252 | 253 |
254 | 255 |
256 | 257 | 258 |
259 | 260 |
261 |
262 | {{!currentPlaying.title}} 263 | 264 | 265 | {{! currentPlaying.artist }} 266 | 267 | 268 | 269 |
270 | 271 |
272 |
273 | 274 |
275 | 276 |
277 |
278 | 279 | {{! currentPostion }} / {{! currentDuration }} 280 |
281 | 282 | 283 | 284 |
285 | 286 |
287 | 添加到歌单 288 | 289 | 290 |
291 | 292 | 310 |
311 |
312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /listen1/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/listen1/tests/__init__.py -------------------------------------------------------------------------------- /listen1/tests/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | 4 | TEST_MODULES = [ 5 | 'list', 6 | 'your', 7 | 'test', 8 | 'modules', 9 | 'test.test_something', 10 | ] 11 | 12 | def all(): 13 | try: 14 | return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES) 15 | except AttributeError as e: 16 | if "'module' object has no attribute 'test_" in str(e): 17 | # most likely because of an import error 18 | for m in TEST_MODULES: 19 | __import__(m, globals(), locals()) 20 | raise 21 | 22 | if __name__ == '__main__': 23 | import tornado.testing 24 | tornado.testing.main() 25 | -------------------------------------------------------------------------------- /listen1/trayshell/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, unicode_literals 3 | 4 | __version__ = '0.1.0' 5 | 6 | APP_NAME = 'Listen 1' 7 | -------------------------------------------------------------------------------- /listen1/trayshell/launcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, unicode_literals 3 | 4 | from PySide.QtGui import * 5 | from PySide.QtCore import * 6 | 7 | from trayshell.shell_pyside import Shell 8 | from multiprocessing import freeze_support 9 | 10 | def main(): 11 | shell = Shell() 12 | shell.run() 13 | 14 | if __name__ == '__main__': 15 | freeze_support() 16 | main() 17 | -------------------------------------------------------------------------------- /listen1/trayshell/shell_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, unicode_literals 3 | 4 | from abc import ABCMeta, abstractmethod 5 | 6 | 7 | class ShellBase(object): 8 | 9 | def __init__(self): 10 | pass 11 | 12 | @abstractmethod 13 | def run(self): 14 | """ Starts up the shell. 15 | """ 16 | pass 17 | -------------------------------------------------------------------------------- /listen1/trayshell/shell_pyside.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, print_function, unicode_literals 3 | 4 | import sys 5 | import logging 6 | 7 | 8 | from PySide.QtGui import QMainWindow, QAction, QMenu, QSystemTrayIcon,\ 9 | QApplication, QIcon, QDesktopServices, QMessageBox 10 | from PySide.QtCore import QThread, QUrl 11 | 12 | from trayshell.shell_base import ShellBase 13 | from trayshell.utils import resource_path 14 | import tornado 15 | 16 | from app import main as webmain 17 | 18 | _logger = logging.getLogger(__name__) 19 | 20 | 21 | class MyWorkerThread(QThread): 22 | def __init__(self): 23 | QThread.__init__(self) 24 | 25 | def run(self): 26 | print('thread running') 27 | webmain() 28 | 29 | 30 | class MainWnd(QMainWindow): 31 | def __init__(self, shell, icon): 32 | super(MainWnd, self).__init__() 33 | self._shell = shell 34 | self.icon = icon 35 | self.context_menu = None 36 | self.tray_icon = None 37 | self.myProcess = None 38 | 39 | if not QSystemTrayIcon.isSystemTrayAvailable(): 40 | msg = "I couldn't detect any system tray on this system." 41 | _logger.error(msg) 42 | QMessageBox.critical(None, "Listen 1", msg) 43 | sys.exit(1) 44 | 45 | self.init_ui() 46 | 47 | def init_ui(self): 48 | self.setWindowIcon(self.icon) 49 | self.setWindowTitle('Listen 1') 50 | 51 | self.create_tray_icon(self.icon) 52 | self.tray_icon.show() 53 | self.on_start() 54 | 55 | def on_tray_activated(self, reason=None): 56 | _logger.debug("Tray icon activated.") 57 | 58 | def on_start(self): 59 | self.myProcess = MyWorkerThread() 60 | self.myProcess.start() 61 | 62 | QDesktopServices.openUrl('http://localhost:8888/') 63 | 64 | def on_open(self): 65 | if sys.platform.startswith('darwin'): 66 | url = '/Applications/Listen 1.app/Contents/MacOS/media/music/' 67 | QDesktopServices.openUrl(QUrl.fromLocalFile(url)) 68 | else: 69 | QDesktopServices.openUrl(QUrl.fromLocalFile('./media/music')) 70 | 71 | def on_quit(self): 72 | if self.myProcess is not None: 73 | # graceful shutdown web server 74 | tornado.ioloop.IOLoop.instance().stop() 75 | self._shell.quit_app() 76 | 77 | def create_actions(self): 78 | """ Creates QAction object for binding event handlers. 79 | """ 80 | self.start_action = QAction( 81 | "&打开 Listen 1", self, triggered=self.on_start) 82 | self.open_action = QAction( 83 | "&离线音乐文件夹", self, triggered=self.on_open) 84 | self.quit_action = QAction( 85 | "&退出", self, triggered=self.on_quit) 86 | 87 | def create_context_menu(self): 88 | menu = QMenu(self) 89 | menu.addAction(self.start_action) 90 | menu.addAction(self.open_action) 91 | menu.addAction(self.quit_action) 92 | return menu 93 | 94 | def create_tray_icon(self, icon): 95 | self.create_actions() 96 | self.context_menu = self.create_context_menu() 97 | self.tray_icon = QSystemTrayIcon(self) 98 | self.tray_icon.setContextMenu(self.context_menu) 99 | self.tray_icon.setIcon(icon) 100 | self.tray_icon.activated.connect(self.on_tray_activated) 101 | 102 | 103 | class Shell(ShellBase): 104 | """ Shell implementation using PySide 105 | """ 106 | 107 | def __init__(self): 108 | super(Shell, self).__init__() 109 | self.app = QApplication(sys.argv) 110 | self.app.setQuitOnLastWindowClosed(False) # 1 111 | self.icon = QIcon(resource_path('res/icon.png')) 112 | self.menu = None 113 | self.wnd = MainWnd(self, self.icon) 114 | 115 | def quit_app(self): 116 | self.app.quit() 117 | 118 | def run(self): 119 | _logger.info("Shell is running...") 120 | self.app.exec_() 121 | 122 | 123 | if __name__ == '__main__': 124 | shell = Shell() 125 | shell.run() 126 | -------------------------------------------------------------------------------- /listen1/trayshell/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, unicode_literals 3 | 4 | import os 5 | import sys 6 | 7 | def resource_path(relative): 8 | """ Gets the resource's absolute path. 9 | 10 | :param relative: the relative path to the resource file. 11 | :return: the absolute path to the resource file. 12 | """ 13 | if hasattr(sys, "_MEIPASS"): 14 | return os.path.join(sys._MEIPASS, relative) 15 | 16 | abspath = os.path.abspath(os.path.join(__file__, "..")) 17 | abspath = os.path.dirname(abspath) 18 | return os.path.join(abspath, relative) 19 | 20 | -------------------------------------------------------------------------------- /listen1/urls.py: -------------------------------------------------------------------------------- 1 | from handlers.douban import DBFavoriteHandler, DBLoginHandler, \ 2 | ValidCodeHandler, DBLogoutHandler 3 | from handlers.home import HomeHandler 4 | from handlers.player import AlbumHandler, ArtistHandler 5 | from handlers.playlist import AddMyPlaylistHandler, ClonePlaylistHandler, \ 6 | CreateMyPlaylistHandler, PlaylistHandler, \ 7 | RemoveMyPlaylistHandler, RemoveTrackHandler, \ 8 | ShowMyPlaylistHandler, ShowPlaylistHandler 9 | from handlers.search import SearchHandler 10 | from handlers.trackfile import TrackFileHandler 11 | 12 | 13 | url_patterns = [ 14 | (r"/", HomeHandler), 15 | 16 | (r"/search", SearchHandler), 17 | 18 | # player handlers 19 | (r"/playlist", PlaylistHandler), 20 | (r"/artist", ArtistHandler), 21 | (r"/album", AlbumHandler), 22 | 23 | # track proxy 24 | (r"/track_file", TrackFileHandler), 25 | 26 | # playlist 27 | (r"/show_playlist", ShowPlaylistHandler), 28 | (r"/add_myplaylist", AddMyPlaylistHandler), 29 | (r"/create_myplaylist", CreateMyPlaylistHandler), 30 | (r"/show_myplaylist", ShowMyPlaylistHandler), 31 | (r"/remove_track_from_myplaylist", RemoveTrackHandler), 32 | (r"/remove_myplaylist", RemoveMyPlaylistHandler), 33 | (r"/clone_playlist", ClonePlaylistHandler), 34 | 35 | # douban handlers 36 | (r"/dbvalidcode", ValidCodeHandler), 37 | (r"/dblogin", DBLoginHandler), 38 | (r"/dblogout", DBLogoutHandler), 39 | (r"/dbfav", DBFavoriteHandler), 40 | ] 41 | -------------------------------------------------------------------------------- /package_resource/mac/dmg-resource/listen1.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "title": "Listen 1", 4 | 5 | "icon": "listen1_install_disk_icon.png.icns", 6 | 7 | "background": "listen1_install_background.png", 8 | 9 | "icon-size": 80, 10 | 11 | "contents": [ 12 | 13 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" }, 14 | 15 | { "x": 192, "y": 344, "type": "file", "path": "Listen 1.app" } 16 | 17 | ] 18 | 19 | } 20 | -------------------------------------------------------------------------------- /package_resource/mac/dmg-resource/listen1_install_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/package_resource/mac/dmg-resource/listen1_install_background.png -------------------------------------------------------------------------------- /package_resource/mac/dmg-resource/listen1_install_disk_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/package_resource/mac/dmg-resource/listen1_install_disk_icon.png -------------------------------------------------------------------------------- /package_resource/mac/dmg-resource/listen1_install_disk_icon.png.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppproxy/listen1/6c2d53a34da756ddd18b66017df5ff47bc67bfc1/package_resource/mac/dmg-resource/listen1_install_disk_icon.png.icns -------------------------------------------------------------------------------- /package_resource/windows/listen1.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /readme_mac.md: -------------------------------------------------------------------------------- 1 | Mac Build Readme 2 | ================= 3 | 4 | 准备 5 | ---- 6 | 1. pip install PySide pyinstaller 7 | 8 | 2. npm install appdmg -g 9 | 10 | 方法 11 | ---- 12 | 1. pyinstaller listen1.spec --clean -y 13 | 14 | 2. cd listen1/dist/Listen\ 1 15 | 16 | 2. cp -r Listen\ 1.app ../../package_resource/mac/dmg-resource/ 17 | 18 | 3. cd package_resource/mac/dmg_resource 19 | 20 | 4. appdmg listen1.json ~/Desktop/listen1.dmg 21 | 22 | 5. 可以参考make.sh,把上述过程自动化 23 | 24 | 可能出现的错误 25 | ------------- 26 | pyside dylib not found: 27 | cd site-packges, use install_name_tool to change @rpath to current directory. 28 | 29 | -------------------------------------------------------------------------------- /readme_windows.md: -------------------------------------------------------------------------------- 1 | Windows Build Readme 2 | ===================== 3 | 4 | 准备 5 | ----- 6 | 1. 包含pyinstaller和PySide的pip环境 7 | 8 | 2. wix (windows打包工具) 9 | 10 | 11 | 方法 12 | ----- 13 | 1. 用pyinstaller生成dist文件 14 | 15 | 2. 用heat命令对dist下的文件生成wix的fragment文件 16 | heat dir "Listen 1" -cg "projectFiles" -ag -out projectFiles.wxs 17 | 18 | 3. 修改生成projectFiles.wxs里的安装路径,把TARGETDIR替换为实际目录,比如ProgramFilesFolder 19 | move projectFiles.wxs ./Listen 1 20 | 21 | 4. candle编译wxs 22 | candle.exe listen1.wxs 23 | candle.exe projectFiles.wxs 24 | 25 | 4. light编译wxobj 26 | light.exe listen1.wixobj projectFiles.wixobj -o c:\listen1.msi 27 | 28 | 29 | 注意事项 30 | -------- 31 | 32 | 1. 避免在有中文的路径下运行,可能导致文件找不到 33 | 34 | --------------------------------------------------------------------------------