├── pyportify ├── __init__.py ├── static │ ├── img │ │ ├── g.png │ │ ├── s.png │ │ ├── appjs.png │ │ ├── nodejs.png │ │ ├── angularjs.png │ │ ├── big_cover.png │ │ ├── no_album.png │ │ ├── glyphicons-halflings.png │ │ ├── glyphicons-halflings-white.png │ │ └── portify.svg │ ├── icons │ │ ├── 16.png │ │ ├── 32.png │ │ ├── 64.png │ │ └── 128.png │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── Flat-UI-Icons.eot │ │ ├── Flat-UI-Icons.ttf │ │ ├── Flat-UI-Icons.woff │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── Flat-UI-Icons.svg │ │ └── Flat-UI-Icons.dev.svg │ ├── partials │ │ ├── fancy_process.html │ │ ├── welcome.html │ │ ├── google_login.html │ │ ├── playlists.html │ │ ├── about.html │ │ ├── spotify_login.html │ │ └── process.html │ ├── index.html │ ├── js │ │ ├── flatui-checkbox.js │ │ ├── app.js │ │ ├── flatui-radio.js │ │ ├── controllers.js │ │ ├── imagesloaded.pkgd.min.js │ │ └── jquery-ui-1.10.3.custom.min.js │ └── css │ │ ├── app.css │ │ ├── bootstrap-responsive.min.css │ │ └── font-awesome.min.css ├── spotify_appkey.key ├── pkcs1 │ ├── defaults.py │ ├── __init__.py │ ├── mgf.py │ ├── exceptions.py │ ├── primitives.py │ ├── rsaes_oaep.py │ ├── primes.py │ └── keys.py ├── server.py ├── middlewares.py ├── serializers.py ├── gpsoauth │ ├── util.py │ ├── google.py │ └── __init__.py ├── util.py ├── copy_all.py ├── spotify.py ├── tests.py ├── google.py └── app.py ├── setup.cfg ├── requirements.txt ├── docker-compose.yml ├── .gitignore ├── MANIFEST.in ├── tox.ini ├── make_exe.sh ├── Dockerfile ├── .vscode ├── settings.json └── tasks.json ├── try_spotify.py ├── setup.py ├── pyportify.spec ├── README.md ├── .travis.yml ├── try_google_music.py └── LICENSE.txt /pyportify/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.4.1' 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==2.3.10 2 | requests==2.18.4 3 | six==1.11.0 4 | certifi>=2017.11.5 5 | -------------------------------------------------------------------------------- /pyportify/static/img/g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/g.png -------------------------------------------------------------------------------- /pyportify/static/img/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/s.png -------------------------------------------------------------------------------- /pyportify/spotify_appkey.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/spotify_appkey.key -------------------------------------------------------------------------------- /pyportify/static/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/icons/16.png -------------------------------------------------------------------------------- /pyportify/static/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/icons/32.png -------------------------------------------------------------------------------- /pyportify/static/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/icons/64.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "3132:3132" 7 | -------------------------------------------------------------------------------- /pyportify/static/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/icons/128.png -------------------------------------------------------------------------------- /pyportify/static/img/appjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/appjs.png -------------------------------------------------------------------------------- /pyportify/static/img/nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/nodejs.png -------------------------------------------------------------------------------- /pyportify/static/img/angularjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/angularjs.png -------------------------------------------------------------------------------- /pyportify/static/img/big_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/big_cover.png -------------------------------------------------------------------------------- /pyportify/static/img/no_album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/no_album.png -------------------------------------------------------------------------------- /pyportify/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /pyportify/static/fonts/Flat-UI-Icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/fonts/Flat-UI-Icons.eot -------------------------------------------------------------------------------- /pyportify/static/fonts/Flat-UI-Icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/fonts/Flat-UI-Icons.ttf -------------------------------------------------------------------------------- /pyportify/static/fonts/Flat-UI-Icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/fonts/Flat-UI-Icons.woff -------------------------------------------------------------------------------- /pyportify/pkcs1/defaults.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | default_crypto_random = random.SystemRandom() 4 | default_pseudo_random = random.Random() 5 | -------------------------------------------------------------------------------- /pyportify/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /pyportify/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /pyportify/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /pyportify/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | *.swp 3 | *.pyc 4 | .idea 5 | .tox 6 | build 7 | dist 8 | pyportify.egg-info 9 | 10 | pyportify.zip 11 | pyportify-* 12 | -------------------------------------------------------------------------------- /pyportify/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rckclmbr/pyportify/HEAD/pyportify/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include pyportify/static * 2 | include pyportify/spotify_appkey.key 3 | include requirements.txt 4 | include LICENSE.txt 5 | include README.md -------------------------------------------------------------------------------- /pyportify/pkcs1/__init__.py: -------------------------------------------------------------------------------- 1 | from . import rsaes_oaep # noqa: F401 2 | from . import keys # noqa: F401 3 | from . import primitives # noqa: F401 4 | 5 | __VERSION__ = (0, 9, 4) 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36 3 | recreate = True 4 | 5 | [testenv] 6 | deps= 7 | unittest2 8 | flake8 9 | -rrequirements.txt 10 | commands= 11 | unit2 discover [] 12 | flake8 --exclude '.git,__pycache__,.tox,gpsoauth,pkcs1' pyportify 13 | -------------------------------------------------------------------------------- /make_exe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #PYTHONPATH=. pyinstaller --onefile pyportify/views.py 4 | #PYTHONPATH=. pyinstaller --onefile pyportify/copy_all.py 5 | PYTHONPATH=. pyinstaller pyportify.spec 6 | #mv dist/copy_all dist/pyportify-copyall 7 | #cp -R pyportify/static dist/ 8 | 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | MAINTAINER Josh Braegger 4 | 5 | ADD . /app/ 6 | RUN pip install -r /app/requirements.txt && \ 7 | pip install /app && \ 8 | rm -r /root/.cache 9 | 10 | EXPOSE 3132 11 | CMD ["/usr/local/bin/pyportify"] 12 | 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.unitTest.unittestArgs": [ 3 | "-v", 4 | "-s", 5 | ".", 6 | "-p", 7 | "test*.py" 8 | ], 9 | "python.unitTest.pyTestEnabled": false, 10 | "python.unitTest.nosetestsEnabled": false, 11 | "python.unitTest.unittestEnabled": true, 12 | "python.linting.flake8Enabled": true, 13 | "python.linting.enabled": true, 14 | } -------------------------------------------------------------------------------- /pyportify/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | from pyportify import app 5 | 6 | 7 | def main(): 8 | loop = asyncio.get_event_loop() 9 | handler = loop.run_until_complete(app.setup(loop)) 10 | try: 11 | loop.run_forever() 12 | except KeyboardInterrupt: 13 | loop.run_until_complete(handler.finish_connections()) 14 | loop.close() 15 | 16 | 17 | if __name__ == "__main__": 18 | main() 19 | -------------------------------------------------------------------------------- /pyportify/middlewares.py: -------------------------------------------------------------------------------- 1 | 2 | def IndexMiddleware(index='index.html'): 3 | async def middleware_factory(app, handler): 4 | async def index_handler(request): 5 | try: 6 | filename = request.match_info['filename'] 7 | if not filename: 8 | filename = index 9 | if filename.endswith('/'): 10 | filename += index 11 | request.match_info['filename'] = filename 12 | except KeyError: 13 | pass 14 | ret = await handler(request) 15 | return ret 16 | return index_handler 17 | return middleware_factory 18 | -------------------------------------------------------------------------------- /pyportify/static/partials/fancy_process.html: -------------------------------------------------------------------------------- 1 |
{{status}}
2 |
3 |

All playlists transfered.

4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /pyportify/serializers.py: -------------------------------------------------------------------------------- 1 | class Track(): 2 | artist = "" 3 | name = "" 4 | track_id = "" 5 | 6 | def __init__(self, artist, name, track_id=""): 7 | self.artist = artist 8 | self.name = name 9 | self.track_id = track_id 10 | 11 | @classmethod 12 | def from_spotify(cls, track): 13 | track_id = track.get("id") 14 | name = track.get("name") 15 | artist = "" 16 | if "artists" in track: 17 | artist = track["artists"][0]["name"] 18 | 19 | return cls(artist, name, track_id) 20 | 21 | @classmethod 22 | def from_gpm(cls, track): 23 | return cls( 24 | track.get("artist"), 25 | track.get("title"), 26 | track.get("storeId") 27 | ) 28 | -------------------------------------------------------------------------------- /pyportify/pkcs1/mgf.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from .primitives import integer_ceil, i2osp 4 | 5 | 6 | def mgf1(mgf_seed, mask_len, hash_class=hashlib.sha1): 7 | ''' 8 | Mask Generation Function v1 from the PKCS#1 v2.0 standard. 9 | 10 | mgs_seed - the seed, a byte string 11 | mask_len - the length of the mask to generate 12 | hash_class - the digest algorithm to use, default is SHA1 13 | 14 | Return value: a pseudo-random mask, as a byte string 15 | ''' 16 | h_len = hash_class().digest_size 17 | if mask_len > 0x10000: 18 | raise ValueError('mask too long') 19 | T = b'' 20 | for i in range(0, integer_ceil(mask_len, h_len)): 21 | C = i2osp(i, 4) 22 | T = T + hash_class(mgf_seed + C).digest() 23 | return T[:mask_len] 24 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build", 8 | "type": "shell", 9 | "command": "${config:python.pythonPath}", 10 | "args": [ 11 | "setup.py", 12 | "sdist", 13 | "bdist_wheel" 14 | ], 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "label": "Publish", 22 | "type": "shell", 23 | "command": "twine", 24 | "args": [ 25 | "upload", 26 | "dist/*" 27 | ] 28 | }, 29 | ], 30 | } -------------------------------------------------------------------------------- /pyportify/pkcs1/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class PKCS1BaseException(Exception): 3 | pass 4 | 5 | 6 | class DecryptionError(PKCS1BaseException): 7 | pass 8 | 9 | 10 | class MessageTooLong(PKCS1BaseException): 11 | pass 12 | 13 | 14 | class WrongLength(PKCS1BaseException): 15 | pass 16 | 17 | 18 | class MessageTooShort(PKCS1BaseException): 19 | pass 20 | 21 | 22 | class InvalidSignature(PKCS1BaseException): 23 | pass 24 | 25 | 26 | class RSAModulusTooShort(PKCS1BaseException): 27 | pass 28 | 29 | 30 | class IntegerTooLarge(PKCS1BaseException): 31 | pass 32 | 33 | 34 | class MessageRepresentativeOutOfRange(PKCS1BaseException): 35 | pass 36 | 37 | 38 | class CiphertextRepresentativeOutOfRange(PKCS1BaseException): 39 | pass 40 | 41 | 42 | class SignatureRepresentativeOutOfRange(PKCS1BaseException): 43 | pass 44 | 45 | 46 | class EncodingError(PKCS1BaseException): 47 | pass 48 | -------------------------------------------------------------------------------- /pyportify/static/partials/welcome.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Welcome.

4 |
5 |
6 |

Pyportify can help you move all your beloved playlists from Spotify to Google Music: All Access

7 | However by using Pyportify you may violate both Spotify's and Google's Terms of Service. You agree that 8 | you are using Pyportify on your own risk. The author does not accept liability (as far as permitted by law) for any loss arising from any use of this tool. 9 | If you choose not to agree to these terms, then you may not use this tool. 10 |

11 | Get started.

12 |
13 | 16 |
17 | -------------------------------------------------------------------------------- /pyportify/gpsoauth/util.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import sys 3 | 4 | PY3 = sys.version[0] == '3' 5 | 6 | 7 | def bytes_to_long(s): 8 | return int.from_bytes(s, "big") 9 | 10 | 11 | def long_to_bytes(lnum, padmultiple=1): 12 | """Packs the lnum (which must be convertable to a long) into a 13 | byte string 0 padded to a multiple of padmultiple bytes in size. 0 14 | means no padding whatsoever, so that packing 0 result in an empty 15 | string. The resulting byte string is the big-endian two's 16 | complement representation of the passed in long.""" 17 | 18 | # source: http://stackoverflow.com/a/14527004/1231454 19 | 20 | if lnum == 0: 21 | return b'\0' * padmultiple 22 | elif lnum < 0: 23 | raise ValueError("Can only convert non-negative numbers.") 24 | s = hex(lnum)[2:] 25 | s = s.rstrip('L') 26 | if len(s) & 1: 27 | s = '0' + s 28 | s = binascii.unhexlify(s) 29 | if (padmultiple != 1) and (padmultiple != 0): 30 | filled_so_far = len(s) % padmultiple 31 | if filled_so_far != 0: 32 | s = b'\0' * (padmultiple - filled_so_far) + s 33 | return s 34 | -------------------------------------------------------------------------------- /pyportify/static/partials/google_login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Login to Google 6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | Heads up! Please make sure you use a Google account with Music: All Access. 19 |
20 |
21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /pyportify/static/partials/playlists.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Your Spotify Plalists 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 |
NameTransfer to Google Music?
{{playlist.name}} 16 | 17 |
21 |
22 |
23 |
24 |
select all
25 |
26 | Ready? Start Transfer 27 |
28 |
29 |
-------------------------------------------------------------------------------- /try_spotify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import pathlib 5 | from pyportify.spotify import SpotifyClient, SpotifyQuery 6 | from aiohttp import ClientSession 7 | 8 | 9 | async def run(): 10 | # https://developer.spotify.com/web-api/console/get-playlists/ 11 | oauth_token = pathlib.Path(pathlib.Path.home(), 12 | "secrets/spotify_access_token.txt") 13 | with ClientSession() as session: 14 | c = SpotifyClient(session, oauth_token) 15 | logged_in = await c.loggedin() 16 | if not logged_in: 17 | print("not logged in") 18 | return 19 | print("Logged in") 20 | 21 | # playlists = await c.fetch_spotify_playlists() 22 | # sp_playlist = playlists[0] 23 | sp_playlist_uri = 'spotify:user:22ujgyiomxbgggsb7mvnorh7q:playlist:3OVXBy5QDsx1jdSHrkAu1L' # noqa 24 | pl = await c.fetch_playlist(sp_playlist_uri) 25 | tracks = pl['tracks']['items'] 26 | for i, sp_track in enumerate(tracks): 27 | query = SpotifyQuery(i, sp_playlist_uri, sp_track, len(tracks)) 28 | query.search_query() 29 | # print(sp_playlist["uri"]) 30 | 31 | 32 | def main(): 33 | loop = asyncio.get_event_loop() 34 | loop.run_until_complete(run()) 35 | loop.close() 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /pyportify/gpsoauth/google.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | 4 | from pyportify.pkcs1.keys import RsaPublicKey 5 | from pyportify.pkcs1 import rsaes_oaep 6 | 7 | from .util import bytes_to_long, long_to_bytes 8 | 9 | 10 | def key_from_b64(b64_key): 11 | binaryKey = base64.b64decode(b64_key) 12 | 13 | i = bytes_to_long(binaryKey[:4]) 14 | mod = bytes_to_long(binaryKey[4:4+i]) 15 | 16 | j = bytes_to_long(binaryKey[i+4:i+4+4]) 17 | exponent = bytes_to_long(binaryKey[i+8:i+8+j]) 18 | 19 | key = RsaPublicKey(mod, exponent) 20 | 21 | return key 22 | 23 | 24 | def key_to_struct(key): 25 | mod = long_to_bytes(key.n) 26 | exponent = long_to_bytes(key.e) 27 | 28 | return b'\x00\x00\x00\x80' + mod + b'\x00\x00\x00\x03' + exponent 29 | 30 | 31 | def parse_auth_response(text): 32 | response_data = {} 33 | for line in text.split('\n'): 34 | if not line: 35 | continue 36 | 37 | key, _, val = line.partition('=') 38 | response_data[key] = val 39 | 40 | return response_data 41 | 42 | 43 | def signature(email, password, key): 44 | signature = bytearray(b'\x00') 45 | 46 | struct = key_to_struct(key) 47 | signature.extend(hashlib.sha1(struct).digest()[:4]) 48 | 49 | message = (email + u'\x00' + password).encode('utf-8') 50 | encrypted_login = rsaes_oaep.encrypt(key, message) 51 | 52 | signature.extend(encrypted_login) 53 | 54 | return base64.urlsafe_b64encode(signature) 55 | -------------------------------------------------------------------------------- /pyportify/static/partials/about.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

{{app_name}}

5 |

6 | Author: Josh Braegger
7 | Based on the original nodejs Portify by Sebastian Mauer
8 | Licensed under the terms of the Apache 2.0 License

9 | Available on GitHub: http://github.com/rckclmbr/pyportify 10 |

11 |
12 | 13 | Contributors:
14 | Eric Barmeyer

15 | All trademarks are the property of their respective owners. 16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 | 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from distutils.core import setup 4 | from setuptools import find_packages 5 | import os 6 | import pathlib 7 | import re 8 | 9 | basepath = os.path.dirname(__file__) 10 | requirements_txt = os.path.join(basepath, "requirements.txt") 11 | 12 | with open(requirements_txt) as reqs: 13 | install_requires = [ 14 | line for line in reqs.read().split('\n') 15 | if (line and not line.startswith('git+') and not line.startswith('--')) 16 | ] 17 | 18 | 19 | def get_version(filename): 20 | with open(filename) as r: 21 | metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", r.read())) 22 | return metadata['version'] 23 | 24 | 25 | args = dict( 26 | name='pyportify', 27 | version=get_version("pyportify/__init__.py"), 28 | author='Josh Braegger', 29 | author_email='rckclmbr@gmail.com', 30 | packages=find_packages(), 31 | include_package_data=True, 32 | url='https://github.com/rckclmbr/pyportify', 33 | license='Apache 2.0', 34 | description='App to transfer your spotify playlists to Google Play ' 35 | 'Music', 36 | long_description=pathlib.Path('README.md').read_text(), 37 | long_description_content_type='text/markdown', 38 | classifiers=['Environment :: Web Environment'], 39 | entry_points={ 40 | 'console_scripts': ['pyportify = pyportify.server:main', 41 | 'pyportify-copyall = pyportify.copy_all:main']}, 42 | data_files=(('', ["LICENSE.txt"]),), 43 | zip_safe=False, 44 | install_requires=install_requires) 45 | 46 | setup(**args) 47 | -------------------------------------------------------------------------------- /pyportify/util.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import sys 3 | 4 | from difflib import SequenceMatcher as SM 5 | 6 | 7 | def uprint(*objects, sep=' ', end='\n', file=sys.stdout): 8 | enc = file.encoding 9 | if enc == 'UTF-8': 10 | print(*objects, sep=sep, end=end, file=file) 11 | else: 12 | def f(obj): 13 | return str(obj) \ 14 | .encode(enc, errors='backslashreplace') \ 15 | .decode(enc) 16 | print(*map(f, objects), sep=sep, end=end, file=file) 17 | 18 | 19 | def grouper(iterable, n): 20 | it = iter(iterable) 21 | while True: 22 | chunk = tuple(itertools.islice(it, n)) 23 | if not chunk: 24 | return 25 | yield chunk 26 | 27 | 28 | def get_similarity(s1, s2): 29 | """ 30 | Return similarity of both strings as a float between 0 and 1 31 | """ 32 | return SM(None, s1, s2).ratio() 33 | 34 | 35 | def find_closest_match(target_track, tracks): 36 | """ 37 | Return closest match to target track 38 | """ 39 | track = None 40 | # Get a list of (track, artist match ratio, name match ratio) 41 | tracks_with_match_ratio = [( 42 | track, 43 | get_similarity(target_track.artist, track.artist), 44 | get_similarity(target_track.name, track.name), 45 | ) for track in tracks] 46 | # Sort by artist then by title 47 | sorted_tracks = sorted( 48 | tracks_with_match_ratio, 49 | key=lambda t: (t[1], t[2]), 50 | reverse=True # Descending, highest match ratio first 51 | ) 52 | if sorted_tracks: 53 | track = sorted_tracks[0][0] # Closest match to query 54 | return track 55 | -------------------------------------------------------------------------------- /pyportify/static/partials/spotify_login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Login to Spotify 6 |
7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | Heads up! To get an OAuth Token: 17 |
    18 |
  1. Visit this page
  2. 19 |
  3. Click "Get OAuth Token"
  4. 20 |
  5. If the token is already there, clear it. We want to make sure you have the proper permissions
  6. 21 |
  7. Check "playlist-read-private", "playlist-read-collaborative", and "user-library-read"
  8. 22 |
  9. Click "Request Token"
  10. 23 |
  11. Either login, or if you're already logged in click "Okay"
  12. 24 |
  13. Copy the token in the "OAuth Token" field
  14. 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /pyportify.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | 5 | HIDDEN_IMPORTS = [ 6 | 'aiohttp', 7 | 'pyportify', 8 | 'pyportify.middlewares', 9 | 'pyportify.server', 10 | ] 11 | 12 | a = Analysis(['pyportify/server.py'], 13 | pathex=['/Users/jbraeg/projects/pyportify'], 14 | binaries=None, 15 | datas=[ 16 | ('pyportify/static', 'pyportify'), 17 | ], 18 | hiddenimports=HIDDEN_IMPORTS, 19 | hookspath=[], 20 | runtime_hooks=[], 21 | excludes=[], 22 | win_no_prefer_redirects=False, 23 | win_private_assemblies=False, 24 | cipher=block_cipher) 25 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 26 | exe = EXE(pyz, 27 | a.scripts, 28 | a.binaries, 29 | a.zipfiles, 30 | a.datas, 31 | name='pyportify', 32 | debug=False, 33 | strip=False, 34 | upx=True, 35 | console=True) 36 | 37 | a1 = Analysis(['pyportify/copy_all.py'], 38 | pathex=['/Users/jbraeg/projects/pyportify'], 39 | binaries=None, 40 | datas=None, 41 | hiddenimports=HIDDEN_IMPORTS, 42 | hookspath=[], 43 | runtime_hooks=[], 44 | excludes=[], 45 | win_no_prefer_redirects=False, 46 | win_private_assemblies=False, 47 | cipher=block_cipher) 48 | pyz1 = PYZ(a1.pure, a1.zipped_data, cipher=block_cipher) 49 | exe1 = EXE(pyz, 50 | a1.scripts, 51 | a1.binaries, 52 | a1.zipfiles, 53 | a1.datas, 54 | name='pyportify-copyall', 55 | debug=False, 56 | strip=False, 57 | upx=True, 58 | console=True) 59 | -------------------------------------------------------------------------------- /pyportify/copy_all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import ssl 5 | from getpass import getpass 6 | import sys 7 | 8 | import aiohttp 9 | from aiohttp import ClientSession 10 | import certifi 11 | 12 | from pyportify import app 13 | from pyportify.google import Mobileclient 14 | from pyportify.spotify import SpotifyClient 15 | from pyportify.util import uprint 16 | 17 | try: 18 | input = raw_input 19 | except NameError: 20 | pass 21 | 22 | OAUTH_URL = \ 23 | "https://developer.spotify.com/web-api/console/get-playlist-tracks/" 24 | 25 | 26 | async def start(): 27 | 28 | sslcontext = ssl.create_default_context(cafile=certifi.where()) 29 | conn = aiohttp.TCPConnector(ssl_context=sslcontext) 30 | 31 | with ClientSession(connector=conn) as session: 32 | 33 | google_email = input("Enter Google email address: ") 34 | google_pass = getpass("Enter Google password: ") 35 | 36 | g = Mobileclient(session) 37 | 38 | logged_in = await g.login(google_email, google_pass) 39 | if not logged_in: 40 | uprint("Invalid Google username/password") 41 | sys.exit(1) 42 | 43 | uprint("Go to {0} and get an oauth token".format(OAUTH_URL)) 44 | spotify_token = input("Enter Spotify oauth token: ") 45 | 46 | s = SpotifyClient(session, spotify_token) 47 | 48 | logged_in = await s.loggedin() 49 | if not logged_in: 50 | uprint("Invalid Spotify token") 51 | sys.exit(1) 52 | 53 | playlists = await s.fetch_spotify_playlists() 54 | playlists = [l['uri'] for l in playlists] 55 | await app.transfer_playlists(None, s, g, playlists) 56 | 57 | 58 | def main(): 59 | loop = asyncio.get_event_loop() 60 | try: 61 | loop.run_until_complete(start()) 62 | finally: 63 | loop.close() 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | -------------------------------------------------------------------------------- /pyportify/static/partials/process.html: -------------------------------------------------------------------------------- 1 |
{{status}}
2 |
3 |

All playlists transfered.

4 | Show tracks not found on Google Music
5 | Transfer more playlists
6 | Start over 7 |
8 | 9 |
10 | « Back

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
Name
{{nf.name}}
23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |

{{currentPlaylist.name}}

32 |
33 | Found:
34 | {{currentPlaylist.found}} 35 |
36 |
37 | Not found:
38 | {{currentPlaylist.notfound}} 39 |
40 |
41 | Filtered Karaoke:
42 | {{currentPlaylist.karaoke}} 43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /pyportify/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pyportify 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | Start 19 | 1 20 | 2 21 | 3 22 | 4 23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pyportify 2 | ========= 3 | 4 | [![Build Status](https://travis-ci.org/rckclmbr/pyportify.svg?branch=master)](https://travis-ci.org/rckclmbr/pyportify) 5 | 6 | A port of [portify](https://github.com/mauimauer/portify) to python. 7 | 8 | But it actually works. 9 | 10 | Transfers your Spotify Premium playlists to Google Music: All Access 11 | 12 | By using Pyportify you may violate both Spotify's and Google's Terms of Service. You agree that 13 | you are using Pyportify on your own risk. The author does not accept liability (as far as permitted by law) for any loss arising from any use of this tool. 14 | If you choose not to agree to these terms, then you may not use this tool. 15 | 16 | If you are unable to sign in to your Google account, try using Google App Passwords: https://security.google.com/settings/security/apppasswords 17 | 18 | Download 19 | -------- 20 | 21 | Windows: 22 | 23 | https://github.com/rckclmbr/pyportify/releases/download/v0.4.1/pyportify.zip 24 | 25 | OSX: 26 | 27 | https://github.com/rckclmbr/pyportify/releases/download/v0.4.1/pyportify.dmg 28 | 29 | Install from pypi 30 | ----------------- 31 | 32 | OS X: 33 | 34 | ```bash 35 | $ brew install python3 36 | $ pip3 install pyportify 37 | ``` 38 | 39 | Ubuntu: 40 | 41 | ```bash 42 | sudo apt-get update 43 | sudo apt-get install -y python3-pip 44 | sudo pip3 install pyportify 45 | ``` 46 | 47 | Fedora 48 | 49 | ```bash 50 | sudo yum check-update 51 | sudo pip install pyportify 52 | ``` 53 | 54 | Running 55 | ------- 56 | 57 | ``` 58 | $ pyportify 59 | # Now open a browser to http://localhost:3132 60 | ``` 61 | 62 | EZ 63 | 64 | Alternatively, you can copy all playlists easily using the ```pyportify-copyall``` command: 65 | 66 | ```bash 67 | $ pyportify-copyall 68 | Enter Google email address: example@gmail.com 69 | Enter Google password: 70 | Go to https://developer.spotify.com/web-api/console/get-playlist-tracks/ and get an oauth token 71 | Enter Spotify oauth token: 72 | (transfer music) 73 | ... 74 | ``` 75 | 76 | Or, use Docker: 77 | 78 | ``` 79 | $ docker run -t -i --rm -p 3132:3132 rckclmbr/pyportify 80 | 81 | or 82 | 83 | $ docker run -t -i --rm rckclmbr/pyportify /usr/local/bin/pyportify-copyall 84 | ``` 85 | 86 | License 87 | ------- 88 | 89 | Licensed under the terms of the Apache 2.0 License 90 | All Trademarks are the property of their respective owners. 91 | -------------------------------------------------------------------------------- /pyportify/static/img/portify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 11 | 13 | 14 | 15 | 17 | 18 | 20 | 22 | 24 | 25 | 27 | 33 | 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | language: objective-c 5 | python: 6 | - "3.6" 7 | sudo: required 8 | env: 9 | - TOXENV=py36 10 | install: 11 | - "PYTHONPATH=. pyinstaller --onefile pyportify.spec" 12 | - "hdiutil create dist/pyportify.dmg -srcfolder dist/ -ov" 13 | deploy: 14 | - provider: releases 15 | api_key: 16 | secure: SXoKAyll1TtFrQeOS5i65tzZLdZlXx5jEOnL3tlblmgcEn0sD4iILV/K/qv8KmeE8biBIdbtcqNJPh65covL8Yidaz211Hc4/MHhBU+rfqnrN0EcPR53/zmUgSbzWl7p62zvHdMSmS/iKQKT6Kv3pvyTEDL35oVX8lx0ckzcjkY 17 | skip_cleanup: true 18 | file: 'dist/pyportify.dmg' 19 | on: 20 | all_branches: true 21 | repo: rckclmbr/pyportify 22 | tags: true 23 | - provider: pypi 24 | user: jbraegger 25 | password: 26 | secure: P3dSxH4hgJ6JdnGc4zcCneM/jn2qczOh11uxFSJGHEvkMvL4vOrZ0yOiE8BBInUBeT/4kVfWNuBghibS1P06AlWWvUz66KHc719mpHBA3On+q/0troVXT1Pl9xiEr8ITvQda7wWgSZaxSVpaZnkMJZQQkR3ucc7p6qm4mygPyjs= 27 | on: 28 | all_branches: true 29 | repo: rckclmbr/pyportify 30 | tags: true 31 | - os: linux 32 | python: "3.6" 33 | env: 34 | - TOXENV=py36 35 | language: python 36 | sudo: required 37 | before_install: 38 | - "sudo apt-get update" 39 | - "sudo apt-get install python-all-dev" 40 | - "pip3 install tox" 41 | before_install: 42 | 43 | # Install python 3.6.3 on OS X 44 | # Workaround for Travis's current lack of official support for Python on OS X 45 | # Reference https://github.com/travis-ci/travis-ci/issues/2312 46 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 47 | echo "TRAVIS_PYTHON_VERSION=$TRAVIS_PYTHON_VERSION"; 48 | 49 | echo Updating Homebrew...; 50 | brew update; 51 | 52 | echo Installing pyenv...; 53 | brew unlink pyenv; 54 | brew install pyenv; 55 | 56 | echo Installing pyenv shims...; 57 | eval "$(pyenv init -)" 58 | 59 | new_python_version="3.6.3"; 60 | 61 | echo "Installing Python${new_python_version}..."; 62 | env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install ${new_python_version}; 63 | pyenv versions; 64 | 65 | echo "Making Python${new_python_version} the global python..."; 66 | pyenv global ${new_python_version}; 67 | 68 | echo "python --version" `python --version`; 69 | echo "pip --version" `pip --version`; 70 | 71 | echo Upgrading pip...; 72 | pip install --upgrade pip; 73 | 74 | echo Installing package dependencies...; 75 | pip3 install pyinstaller==3.3 tox; 76 | pip3 install -r requirements.txt; 77 | fi 78 | 79 | script: tox 80 | -------------------------------------------------------------------------------- /try_google_music.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import ssl 5 | import pathlib 6 | import sys 7 | 8 | import aiohttp 9 | from aiohttp import ClientSession 10 | import certifi 11 | 12 | 13 | from pyportify import app 14 | from pyportify.google import Mobileclient 15 | from pyportify.spotify import SpotifyClient 16 | from pyportify.util import uprint 17 | 18 | try: 19 | input = raw_input 20 | except NameError: 21 | pass 22 | 23 | OAUTH_URL = \ 24 | "https://developer.spotify.com/web-api/console/get-playlist-tracks/" 25 | 26 | google_email = "rckclmbr@gmail.com" 27 | google_pass = pathlib.Path( 28 | pathlib.Path.home(), 29 | "secrets/pyportify_google_pass.txt").read_text() 30 | 31 | # https://developer.spotify.com/web-api/console/get-playlists/ 32 | spotify_token = pathlib.Path( 33 | pathlib.Path.home(), 34 | "secrets/pyportify_spotify_token.txt").read_text() 35 | 36 | 37 | async def test_percent_search(g): 38 | ret = await g.search_all_access("Hello % there") 39 | print(ret) 40 | 41 | 42 | async def test_playlist(s, g): 43 | # uri = "spotify:user:lunsku:playlist:0OG0sShb7v1eU5brRfKDpv" 44 | uri = "spotify:user:22ujgyiomxbgggsb7mvnorh7q:playlist:3OVXBy5QDsx1jdSHrkAu1L" # noqa 45 | await app.transfer_playlists(None, s, g, [uri]) 46 | 47 | 48 | async def start(): 49 | 50 | sslcontext = ssl.create_default_context(cafile=certifi.where()) 51 | conn = aiohttp.TCPConnector(ssl_context=sslcontext) 52 | 53 | with ClientSession(connector=conn) as session: 54 | 55 | g = Mobileclient(session) 56 | logged_in = await g.login(google_email, google_pass) 57 | if not logged_in: 58 | uprint("Invalid Google username/password") 59 | sys.exit(1) 60 | 61 | s = SpotifyClient(session, spotify_token) 62 | 63 | logged_in = await s.loggedin() 64 | if not logged_in: 65 | uprint("Invalid Spotify token") 66 | sys.exit(1) 67 | 68 | await test_percent_search(g) 69 | await test_playlist(s, g) 70 | return 71 | 72 | # di = await g.fetch_playlists() 73 | # import pprint 74 | # pprint.pprint(di['data']['items']) 75 | # 76 | # # playlist_id = await g.create_playlist("Test Playlist") 77 | # playlist_id = "2c02eca1-429e-4ce0-a4a8-819415cdee3a" 78 | # await g.add_songs_to_playlist( 79 | # playlist_id, 80 | # ['Twqujxontbfftlzi7hextragxyu'], 81 | # # ['ba3a473e-6309-3814-8c05-b8b6619f38f3'], 82 | # ) 83 | playlists = await s.fetch_spotify_playlists() 84 | playlists = [l['uri'] for l in playlists] 85 | await app.transfer_playlists(None, s, g, playlists) 86 | 87 | 88 | def main(): 89 | loop = asyncio.get_event_loop() 90 | try: 91 | loop.run_until_complete(start()) 92 | finally: 93 | loop.close() 94 | 95 | 96 | if __name__ == '__main__': 97 | main() 98 | -------------------------------------------------------------------------------- /pyportify/pkcs1/primitives.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import operator 3 | import sys 4 | 5 | from functools import reduce 6 | 7 | from .defaults import default_crypto_random 8 | from . import exceptions 9 | 10 | 11 | '''Primitive functions extracted from the PKCS1 RFC''' 12 | 13 | 14 | def integer_ceil(a, b): 15 | '''Return the ceil integer of a div b.''' 16 | quanta, mod = divmod(a, b) 17 | if mod: 18 | quanta += 1 19 | return quanta 20 | 21 | 22 | def integer_byte_size(n): 23 | '''Returns the number of bytes necessary to store the integer n.''' 24 | quanta, mod = divmod(integer_bit_size(n), 8) 25 | if mod or n == 0: 26 | quanta += 1 27 | return quanta 28 | 29 | 30 | def integer_bit_size(n): 31 | '''Returns the number of bits necessary to store the integer n.''' 32 | if n == 0: 33 | return 1 34 | s = 0 35 | while n: 36 | s += 1 37 | n >>= 1 38 | return s 39 | 40 | 41 | def bezout(a, b): 42 | '''Compute the bezout algorithm of a and b, i.e. it returns u, v, p such as: 43 | 44 | p = GCD(a,b) 45 | a * u + b * v = p 46 | 47 | Copied from http://www.labri.fr/perso/betrema/deug/poly/euclide.html. 48 | ''' 49 | u = 1 50 | v = 0 51 | s = 0 52 | t = 1 53 | while b > 0: 54 | q = a // b 55 | r = a % b 56 | a = b 57 | b = r 58 | tmp = s 59 | s = u - q * s 60 | u = tmp 61 | tmp = t 62 | t = v - q * t 63 | v = tmp 64 | return u, v, a 65 | 66 | 67 | def i2osp(x, x_len): 68 | '''Converts the integer x to its big-endian representation of length 69 | x_len. 70 | ''' 71 | if x > 256**x_len: 72 | raise exceptions.IntegerTooLarge 73 | h = hex(x)[2:] 74 | if h[-1] == 'L': 75 | h = h[:-1] 76 | if len(h) & 1 == 1: 77 | h = '0%s' % h 78 | x = binascii.unhexlify(h) 79 | return b'\x00' * int(x_len-len(x)) + x 80 | 81 | 82 | def os2ip(x): 83 | '''Converts the byte string x representing an integer reprented using the 84 | big-endian convient to an integer. 85 | ''' 86 | h = binascii.hexlify(x) 87 | return int(h, 16) 88 | 89 | 90 | def string_xor(a, b): 91 | '''Computes the XOR operator between two byte strings. If the strings are 92 | of different lengths, the result string is as long as the shorter. 93 | ''' 94 | if sys.version_info[0] < 3: 95 | return ''.join((chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b))) 96 | else: 97 | return bytes(x ^ y for (x, y) in zip(a, b)) 98 | 99 | 100 | def product(*args): 101 | '''Computes the product of its arguments.''' 102 | return reduce(operator.__mul__, args) 103 | 104 | 105 | def get_nonzero_random_bytes(length, rnd=default_crypto_random): 106 | ''' 107 | Accumulate random bit string and remove \0 bytes until the needed length 108 | is obtained. 109 | ''' 110 | result = [] 111 | i = 0 112 | while i < length: 113 | rnd = rnd.getrandbits(12*length) 114 | s = i2osp(rnd, 3*length) 115 | s = s.replace('\x00', '') 116 | result.append(s) 117 | i += len(s) 118 | return (''.join(result))[:length] 119 | 120 | 121 | def constant_time_cmp(a, b): 122 | '''Compare two strings using constant time.''' 123 | result = True 124 | for x, y in zip(a, b): 125 | result &= (x == y) 126 | return result 127 | -------------------------------------------------------------------------------- /pyportify/gpsoauth/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from . import google 4 | 5 | 6 | # The key is distirbuted with Google Play Services. 7 | # This one is from version 7.3.29. 8 | b64_key_7_3_29 = (b"AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3" 9 | b"iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pK" 10 | b"RI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/" 11 | b"6rmf5AAAAAwEAAQ==") 12 | 13 | android_key_7_3_29 = google.key_from_b64(b64_key_7_3_29) 14 | 15 | auth_url = 'https://android.clients.google.com/auth' 16 | useragent = 'gpsoauth-portify/1.0' 17 | 18 | 19 | def _perform_auth_request(data): 20 | res = requests.post(auth_url, data, 21 | headers={'User-Agent': useragent}) 22 | 23 | return google.parse_auth_response(res.text) 24 | 25 | 26 | def perform_master_login(email, password, android_id, 27 | service='ac2dm', device_country='us', 28 | operatorCountry='us', lang='en', sdk_version=17): 29 | """ 30 | Perform a master login, which is what Android does when you first add a 31 | Google account. 32 | 33 | Return a dict, eg:: 34 | 35 | { 36 | 'Auth': '...', 37 | 'Email': 'email@gmail.com', 38 | 'GooglePlusUpgrade': '1', 39 | 'LSID': '...', 40 | 'PicasaUser': 'My Name', 41 | 'RopRevision': '1', 42 | 'RopText': ' ', 43 | 'SID': '...', 44 | 'Token': 'oauth2rt_1/...', 45 | 'firstName': 'My', 46 | 'lastName': 'Name', 47 | 'services': 'hist,mail,googleme,...' 48 | } 49 | """ 50 | 51 | data = { 52 | 'accountType': 'HOSTED_OR_GOOGLE', 53 | 'Email': email, 54 | 'has_permission': 1, 55 | 'add_account': 1, 56 | 'EncryptedPasswd': 57 | google.signature(email, password, android_key_7_3_29), 58 | 'service': service, 59 | 'source': 'android', 60 | 'androidId': android_id, 61 | 'device_country': device_country, 62 | 'operatorCountry': device_country, 63 | 'lang': lang, 64 | 'sdk_version': sdk_version, 65 | } 66 | 67 | return _perform_auth_request(data) 68 | 69 | 70 | def perform_oauth(email, master_token, android_id, service, app, client_sig, 71 | device_country='us', operatorCountry='us', lang='en', 72 | sdk_version=17): 73 | """ 74 | Use a master token from master_login to perform OAuth to a specific Google 75 | service. 76 | 77 | Return a dict, eg:: 78 | 79 | { 80 | 'Auth': '...', 81 | 'LSID': '...', 82 | 'SID': '..', 83 | 'issueAdvice': 'auto', 84 | 'services': 'hist,mail,googleme,...' 85 | } 86 | 87 | To authenticate requests to this service, include a header 88 | ``Authorization: GoogleLogin auth=res['Auth']``. 89 | """ 90 | 91 | data = { 92 | 'accountType': 'HOSTED_OR_GOOGLE', 93 | 'Email': email, 94 | 'has_permission': 1, 95 | 'EncryptedPasswd': master_token, 96 | 'service': service, 97 | 'source': 'android', 98 | 'androidId': android_id, 99 | 'app': app, 100 | 'client_sig': client_sig, 101 | 'device_country': device_country, 102 | 'operatorCountry': device_country, 103 | 'lang': lang, 104 | 'sdk_version': sdk_version 105 | } 106 | 107 | return _perform_auth_request(data) 108 | -------------------------------------------------------------------------------- /pyportify/static/js/flatui-checkbox.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * flatui-checkbox.js v0.0.2 3 | * ============================================================ */ 4 | 5 | !function ($) { 6 | 7 | /* CHECKBOX PUBLIC CLASS DEFINITION 8 | * ============================== */ 9 | 10 | var Checkbox = function (element, options) { 11 | this.init(element, options); 12 | } 13 | 14 | Checkbox.prototype = { 15 | 16 | constructor: Checkbox 17 | 18 | , init: function (element, options) { 19 | var $el = this.$element = $(element) 20 | 21 | this.options = $.extend({}, $.fn.checkbox.defaults, options); 22 | $el.before(this.options.template); 23 | this.setState(); 24 | } 25 | 26 | , setState: function () { 27 | var $el = this.$element 28 | , $parent = $el.closest('.checkbox'); 29 | 30 | $el.prop('disabled') && $parent.addClass('disabled'); 31 | $el.prop('checked') && $parent.addClass('checked'); 32 | } 33 | 34 | , toggle: function () { 35 | var ch = 'checked' 36 | , $el = this.$element 37 | , $parent = $el.closest('.checkbox') 38 | , checked = $el.prop(ch) 39 | , e = $.Event('toggle') 40 | 41 | if ($el.prop('disabled') == false) { 42 | $parent.toggleClass(ch) && checked ? $el.removeAttr(ch) : $el.attr(ch, true); 43 | $el.trigger(e).trigger('change'); 44 | } 45 | } 46 | 47 | , setCheck: function (option) { 48 | var d = 'disabled' 49 | , ch = 'checked' 50 | , $el = this.$element 51 | , $parent = $el.closest('.checkbox') 52 | , checkAction = option == 'check' ? true : false 53 | , e = $.Event(option) 54 | 55 | $parent[checkAction ? 'addClass' : 'removeClass' ](ch) && checkAction ? $el.attr(ch, true) : $el.removeAttr(ch); 56 | $el.trigger(e).trigger('change'); 57 | } 58 | 59 | } 60 | 61 | 62 | /* CHECKBOX PLUGIN DEFINITION 63 | * ======================== */ 64 | 65 | var old = $.fn.checkbox 66 | 67 | $.fn.checkbox = function (option) { 68 | return this.each(function () { 69 | var $this = $(this) 70 | , data = $this.data('checkbox') 71 | , options = $.extend({}, $.fn.checkbox.defaults, $this.data(), typeof option == 'object' && option); 72 | if (!data) $this.data('checkbox', (data = new Checkbox(this, options))); 73 | if (option == 'toggle') data.toggle() 74 | if (option == 'check' || option == 'uncheck') data.setCheck(option) 75 | else if (option) data.setState(); 76 | }); 77 | } 78 | 79 | $.fn.checkbox.defaults = { 80 | template: '' 81 | } 82 | 83 | 84 | /* CHECKBOX NO CONFLICT 85 | * ================== */ 86 | 87 | $.fn.checkbox.noConflict = function () { 88 | $.fn.checkbox = old; 89 | return this; 90 | } 91 | 92 | 93 | /* CHECKBOX DATA-API 94 | * =============== */ 95 | 96 | $(document).on('click.checkbox.data-api', '[data-toggle^=checkbox], .checkbox', function (e) { 97 | var $checkbox = $(e.target); 98 | e && e.preventDefault() && e.stopPropagation(); 99 | if (!$checkbox.hasClass('checkbox')) $checkbox = $checkbox.closest('.checkbox'); 100 | $checkbox.find(':checkbox').checkbox('toggle'); 101 | }); 102 | 103 | $(window).on('load', function () { 104 | $('[data-toggle="checkbox"]').each(function () { 105 | var $checkbox = $(this); 106 | $checkbox.checkbox(); 107 | }); 108 | }); 109 | 110 | }(window.jQuery); -------------------------------------------------------------------------------- /pyportify/spotify.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | from pyportify.serializers import Track 4 | 5 | 6 | class SpotifyQuery(): 7 | 8 | def __init__(self, i, sp_playlist_uri, sp_track, track_count): 9 | self.track = Track.from_spotify(sp_track.get("track", {})) 10 | self.i = i 11 | self.playlist_uri = sp_playlist_uri 12 | self.track_count = track_count 13 | 14 | def search_query(self): 15 | track = self.track 16 | if not track.name or not track.artist: 17 | return None 18 | 19 | if track.artist: 20 | search_query = "{0} - {1}".format( 21 | track.artist, 22 | track.name) 23 | else: 24 | search_query = "{0}".format(track.name) 25 | return search_query 26 | 27 | 28 | def encode(values): 29 | return urllib.parse.urlencode(values) 30 | 31 | 32 | class SpotifyClient(object): 33 | 34 | def __init__(self, session, token=None): 35 | self.session = session 36 | self.token = token 37 | 38 | async def loggedin(self): 39 | playlists = await self._http_get( 40 | 'https://api.spotify.com/v1/me/playlists') 41 | if "error" in playlists: 42 | return False 43 | return True 44 | 45 | async def fetch_spotify_playlists(self): 46 | ret_playlists = [{ 47 | "name": "Saved Tracks", 48 | "uri": "saved", 49 | "type": "custom"}] 50 | 51 | url = 'https://api.spotify.com/v1/me/playlists' 52 | playlists = await self._http_get_all(url) 53 | ret_playlists.extend(playlists) 54 | return ret_playlists 55 | 56 | async def _http_get_all(self, url): 57 | ret = [] 58 | while True: 59 | data = await self._http_get(url) 60 | url = data['next'] 61 | ret.extend(data['items']) 62 | if url is None: 63 | break 64 | return ret 65 | 66 | async def fetch_saved_tracks(self): 67 | url = 'https://api.spotify.com/v1/me/tracks' 68 | tracks = await self._http_get_all(url) 69 | return tracks 70 | 71 | async def fetch_playlist_tracks(self, uri): 72 | if uri == 'saved': 73 | ret = await self.fetch_saved_tracks() 74 | return ret 75 | 76 | # spotify:user::playlist: 77 | parts = uri.split(':') 78 | user_id = parts[2] 79 | playlist_id = parts[-1] 80 | 81 | url = 'https://api.spotify.com/v1/users/{0}/playlists/{1}/tracks' \ 82 | .format(user_id, playlist_id) 83 | ret = await self._http_get_all(url) 84 | return ret 85 | 86 | async def fetch_playlist(self, uri): 87 | if uri == 'saved': 88 | return {'name': 'Saved Tracks', 89 | 'uri': uri} 90 | parts = uri.split(':') # spotify:user::playlist: 91 | user_id = parts[2] 92 | playlist_id = parts[-1] 93 | 94 | url = 'https://api.spotify.com/v1/users/{0}/playlists/{1}'.format( 95 | user_id, playlist_id) 96 | ret = await self._http_get(url) 97 | return ret 98 | 99 | async def _http_get(self, url): 100 | headers = {"Authorization": "Bearer {0}".format(self.token), 101 | "Content-type": "application/json"} 102 | res = await self.session.request( 103 | 'GET', 104 | url, 105 | headers=headers, 106 | skip_auto_headers=['Authorization']) 107 | data = await res.json() 108 | if "error" in data: 109 | raise Exception("Error: {0}, url: {1}".format(data, url)) 110 | return data 111 | -------------------------------------------------------------------------------- /pyportify/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyportify import app 3 | from pyportify.serializers import Track 4 | from pyportify.util import find_closest_match 5 | 6 | 7 | class UserScopeTest(unittest.TestCase): 8 | 9 | def test_user_scope(self): 10 | scope = app.user_scope 11 | 12 | assert not scope.google_token 13 | assert not scope.spotify_token 14 | 15 | 16 | class TrackMatchTest(unittest.TestCase): 17 | 18 | def test_artist_match(self): 19 | target_artist = "Target" 20 | target_name = "Songs to Test By" 21 | expected_id = 1 22 | 23 | target_track = Track( 24 | artist=target_artist, 25 | name=target_name 26 | ) 27 | expected_match = Track( 28 | artist=target_artist, 29 | name=target_name, 30 | track_id=expected_id 31 | ) 32 | unexpected_match = Track( 33 | artist="Not Me!", 34 | name=target_name 35 | ) 36 | 37 | match = find_closest_match(target_track, [ 38 | expected_match, 39 | unexpected_match 40 | ]) 41 | 42 | assert match.track_id == expected_id 43 | 44 | def test_artist_match_close_track_name(self): 45 | target_artist = "Target" 46 | target_name = "Songs to Test By" 47 | expected_id = 1 48 | 49 | target_track = Track( 50 | artist=target_artist, 51 | name=target_name 52 | ) 53 | expected_match = Track( 54 | artist=target_artist, 55 | name="Songs to Test With", 56 | track_id=expected_id 57 | ) 58 | unexpected_match = Track( 59 | artist="Not Me, but my track name is closer!", 60 | name=target_name 61 | ) 62 | 63 | match = find_closest_match(target_track, [ 64 | expected_match, 65 | unexpected_match 66 | ]) 67 | 68 | assert match.track_id == expected_id 69 | 70 | def test_close_artist_and_name_match(self): 71 | target_artist = "Target" 72 | target_name = "Songs to Test By" 73 | expected_id = 1 74 | 75 | target_track = Track( 76 | artist=target_artist, 77 | name=target_name 78 | ) 79 | expected_match = Track( 80 | artist="Targ", 81 | name="Songs to Test With", 82 | track_id=expected_id 83 | ) 84 | unexpected_match = Track( 85 | artist="Not Me!", 86 | name=target_name 87 | ) 88 | 89 | match = find_closest_match(target_track, [ 90 | expected_match, 91 | unexpected_match 92 | ]) 93 | 94 | assert match.track_id == expected_id 95 | 96 | def test_multi_artist_match(self): 97 | target_artist = "Target" 98 | target_name = "Songs to Test By" 99 | expected_id = 1 100 | 101 | target_track = Track( 102 | artist=target_artist, 103 | name=target_name 104 | ) 105 | expected_match = Track( 106 | artist=target_artist, 107 | name=target_name, 108 | track_id=expected_id 109 | ) 110 | un_exp_match1 = Track( 111 | artist=target_artist, 112 | name="Songs to Test With?" 113 | ) 114 | un_exp_match2 = Track( 115 | artist=target_artist, 116 | name="Songs to Test With! - ft. Test" 117 | ) 118 | un_exp_match3 = Track( 119 | artist="Not Me!", 120 | name=target_name 121 | ) 122 | 123 | match = find_closest_match(target_track, [ 124 | expected_match, 125 | un_exp_match1, 126 | un_exp_match2, 127 | un_exp_match3 128 | ]) 129 | 130 | assert match.track_id == expected_id 131 | -------------------------------------------------------------------------------- /pyportify/pkcs1/rsaes_oaep.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from . import primitives 4 | from . import exceptions 5 | from . import mgf 6 | from .defaults import default_crypto_random 7 | 8 | 9 | def encrypt(public_key, message, label=b'', hash_class=hashlib.sha1, 10 | mgf=mgf.mgf1, seed=None, rnd=default_crypto_random): 11 | '''Encrypt a byte message using a RSA public key and the OAEP wrapping 12 | algorithm, 13 | 14 | Parameters: 15 | public_key - an RSA public key 16 | message - a byte string 17 | label - a label a per-se PKCS#1 standard 18 | hash_class - a Python class for a message digest algorithme respecting 19 | the hashlib interface 20 | mgf1 - a mask generation function 21 | seed - a seed to use instead of generating it using a random generator 22 | rnd - a random generator class, respecting the random generator 23 | interface from the random module, if seed is None, it is used to 24 | generate it. 25 | 26 | Return value: 27 | the encrypted string of the same length as the public key 28 | ''' 29 | 30 | hash = hash_class() 31 | h_len = hash.digest_size 32 | k = public_key.byte_size 33 | max_message_length = k - 2 * h_len - 2 34 | if len(message) > max_message_length: 35 | raise exceptions.MessageTooLong 36 | hash.update(label) 37 | label_hash = hash.digest() 38 | ps = b'\0' * int(max_message_length - len(message)) 39 | db = b''.join((label_hash, ps, b'\x01', message)) 40 | if not seed: 41 | seed = primitives.i2osp(rnd.getrandbits(h_len*8), h_len) 42 | db_mask = mgf(seed, k - h_len - 1, hash_class=hash_class) 43 | masked_db = primitives.string_xor(db, db_mask) 44 | seed_mask = mgf(masked_db, h_len, hash_class=hash_class) 45 | masked_seed = primitives.string_xor(seed, seed_mask) 46 | em = b''.join((b'\x00', masked_seed, masked_db)) 47 | m = primitives.os2ip(em) 48 | c = public_key.rsaep(m) 49 | output = primitives.i2osp(c, k) 50 | return output 51 | 52 | 53 | def decrypt(private_key, message, label=b'', hash_class=hashlib.sha1, 54 | mgf=mgf.mgf1): 55 | '''Decrypt a byte message using a RSA private key and the OAEP wrapping 56 | algorithm 57 | 58 | Parameters: 59 | public_key - an RSA public key 60 | message - a byte string 61 | label - a label a per-se PKCS#1 standard 62 | hash_class - a Python class for a message digest algorithme respecting 63 | the hashlib interface 64 | mgf1 - a mask generation function 65 | 66 | Return value: 67 | the string before encryption (decrypted) 68 | ''' 69 | hash = hash_class() 70 | h_len = hash.digest_size 71 | k = private_key.byte_size 72 | # 1. check length 73 | if len(message) != k or k < 2 * h_len + 2: 74 | raise ValueError('decryption error') 75 | # 2. RSA decryption 76 | c = primitives.os2ip(message) 77 | m = private_key.rsadp(c) 78 | em = primitives.i2osp(m, k) 79 | # 4. EME-OAEP decoding 80 | hash.update(label) 81 | label_hash = hash.digest() 82 | y, masked_seed, masked_db = em[0], em[1:h_len+1], em[1+h_len:] 83 | if y != b'\x00' and y != 0: 84 | raise ValueError('decryption error') 85 | seed_mask = mgf(masked_db, h_len) 86 | seed = primitives.string_xor(masked_seed, seed_mask) 87 | db_mask = mgf(seed, k - h_len - 1) 88 | db = primitives.string_xor(masked_db, db_mask) 89 | label_hash_prime, rest = db[:h_len], db[h_len:] 90 | i = rest.find(b'\x01') 91 | if i == -1: 92 | raise exceptions.DecryptionError 93 | if rest[:i].strip(b'\x00') != b'': 94 | print(rest[:i].strip(b'\x00')) 95 | raise exceptions.DecryptionError 96 | m = rest[i+1:] 97 | if label_hash_prime != label_hash: 98 | raise exceptions.DecryptionError 99 | return m 100 | -------------------------------------------------------------------------------- /pyportify/static/js/app.js: -------------------------------------------------------------------------------- 1 | angular.module('portify', []). 2 | factory('portifyService', function($rootScope, $http, $q, $location) { 3 | var portifyService = {}; 4 | 5 | //Gets the list of nuclear weapons 6 | portifyService.getSpotifyPlaylists = function() { 7 | var deferred = $q.defer(); 8 | $http.get('/spotify/playlists') 9 | .success(function(data) { 10 | deferred.resolve(data.data); 11 | }) 12 | .error(function(error){ 13 | $scope.error = error; 14 | deferred.reject(); 15 | alert(error); 16 | }); 17 | 18 | return deferred.promise; 19 | }; 20 | 21 | portifyService.startTransfer = function(lists) { 22 | $http({ 23 | url: "/portify/transfer/start", 24 | dataType: "json", 25 | method: "POST", 26 | data: lists, 27 | headers: { 28 | "Content-Type": "application/json; charset=utf-8" 29 | } 30 | }).success(function(response){ 31 | if(response.status == 200) { 32 | console.log("initiated transfer..."); 33 | } else { 34 | if(response.status == 401) 35 | $location.path( "/google/login" ); 36 | else if(response.status == 402) 37 | $location.path( "/spotify/login" ); 38 | else if(response.status == 403) 39 | $location.path( "/spotify/playlists/select" ); 40 | else 41 | $location.path( "/" ); 42 | 43 | } 44 | }).error(function(error){ 45 | console.log(error); 46 | }); 47 | }; 48 | 49 | return portifyService; 50 | }). 51 | factory('context', function($rootScope, $http, $q) { 52 | var items = []; 53 | var context = {}; 54 | 55 | context.addItem = function(item) { 56 | items.push(item); 57 | }; 58 | context.clear = function() { 59 | items = []; 60 | }; 61 | context.removeItem = function(item) { 62 | var index = items.indexOf(item); 63 | items.splice(index, 1); 64 | }; 65 | context.items = function() { 66 | return items; 67 | }; 68 | 69 | return context; 70 | }). 71 | factory('socket', function ($rootScope) { 72 | var socket = new WebSocket('ws://'+window.location.host+'/ws/'); 73 | var callbacks = {}; 74 | socket.onmessage = function (e) { 75 | var data = JSON.parse(e.data); 76 | var currentCallbacks = callbacks[data['eventName']]; 77 | for (var i = 0; i < currentCallbacks.length; i++) { 78 | var callback = currentCallbacks[i]; 79 | var args = arguments; 80 | $rootScope.$apply(function () { 81 | callback.apply(socket, args); 82 | }); 83 | } 84 | }; 85 | return { 86 | on: function (eventName, callback) { 87 | if (!callbacks[eventName]) { 88 | callbacks[eventName] = []; 89 | } 90 | callbacks[eventName].push(callback); 91 | }, 92 | emit: function (eventName, data, callback) { 93 | var data = {'eventName': eventName, 'eventData': data}; 94 | data = JSON.stringify(data); 95 | socket.send(data); 96 | } 97 | }; 98 | }). 99 | config(function($routeProvider, $locationProvider) { 100 | //$locationProvider.html5Mode(true); 101 | $routeProvider. 102 | when('/', {templateUrl: '/partials/welcome.html', controller: WelcomeCtrl}). 103 | when('/about', {templateUrl: '/partials/about.html', controller: AboutCtrl}). 104 | when('/google/login', {templateUrl: '/partials/google_login.html', controller: GoogleLoginCtrl}). 105 | when('/spotify/login', {templateUrl: '/partials/spotify_login.html', controller: SpotifyLoginCtrl}). 106 | when('/spotify/playlists/select', {templateUrl: '/partials/playlists.html', controller: SelectSpotifyCtrl}). 107 | when('/transfer/process_fancy', {templateUrl: '/partials/fancy_process.html', controller: FancyProcessTransferCtrl}). 108 | when('/transfer/process', {templateUrl: '/partials/process.html', controller: ProcessTransferCtrl}). 109 | otherwise({redirectTo: '/'}); 110 | }). 111 | directive('scrollGlue', function() { 112 | return { 113 | priority: 1, 114 | require: ['?ngModel'], 115 | restrict: 'A', 116 | link: function(scope, $el, attrs, ctrls) { 117 | var el = $el[0]; 118 | var lastScroll = -1; 119 | 120 | function scrollToBottom() { 121 | if(el.scrollTop < el.scrollHeight && el.scrollHeight > lastScroll) { 122 | lastScroll = el.scrollHeight; 123 | var targetScroll = el.scrollHeight; 124 | $(el).animate( { 125 | scrollTop: targetScroll 126 | }, 500); 127 | } 128 | } 129 | 130 | scope.$watch(function() { 131 | scrollToBottom(); 132 | }); 133 | } 134 | }}); 135 | -------------------------------------------------------------------------------- /pyportify/static/js/flatui-radio.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * flatui-radio.js v0.0.2 3 | * ============================================================ */ 4 | 5 | !function ($) { 6 | 7 | /* RADIO PUBLIC CLASS DEFINITION 8 | * ============================== */ 9 | 10 | var Radio = function (element, options) { 11 | this.init(element, options); 12 | } 13 | 14 | Radio.prototype = { 15 | 16 | constructor: Radio 17 | 18 | , init: function (element, options) { 19 | var $el = this.$element = $(element) 20 | 21 | this.options = $.extend({}, $.fn.radio.defaults, options); 22 | $el.before(this.options.template); 23 | this.setState(); 24 | } 25 | 26 | , setState: function () { 27 | var $el = this.$element 28 | , $parent = $el.closest('.radio'); 29 | 30 | $el.prop('disabled') && $parent.addClass('disabled'); 31 | $el.prop('checked') && $parent.addClass('checked'); 32 | } 33 | 34 | , toggle: function () { 35 | var d = 'disabled' 36 | , ch = 'checked' 37 | , $el = this.$element 38 | , checked = $el.prop(ch) 39 | , $parent = $el.closest('.radio') 40 | , $parentWrap = $el.closest('form').length ? $el.closest('form') : $el.closest('body') 41 | , $elemGroup = $parentWrap.find(':radio[name="' + $el.attr('name') + '"]') 42 | , e = $.Event('toggle') 43 | 44 | $elemGroup.not($el).each(function () { 45 | var $el = $(this) 46 | , $parent = $(this).closest('.radio'); 47 | 48 | if ($el.prop(d) == false) { 49 | $parent.removeClass(ch) && $el.attr(ch, false).trigger('change'); 50 | } 51 | }); 52 | 53 | if ($el.prop(d) == false) { 54 | if (checked == false) $parent.addClass(ch) && $el.attr(ch, true); 55 | $el.trigger(e); 56 | 57 | if (checked !== $el.prop(ch)) { 58 | $el.trigger('change'); 59 | } 60 | } 61 | } 62 | 63 | , setCheck: function (option) { 64 | var ch = 'checked' 65 | , $el = this.$element 66 | , $parent = $el.closest('.radio') 67 | , checkAction = option == 'check' ? true : false 68 | , checked = $el.prop(ch) 69 | , $parentWrap = $el.closest('form').length ? $el.closest('form') : $el.closest('body') 70 | , $elemGroup = $parentWrap.find(':radio[name="' + $el['attr']('name') + '"]') 71 | , e = $.Event(option) 72 | 73 | $elemGroup.not($el).each(function () { 74 | var $el = $(this) 75 | , $parent = $(this).closest('.radio'); 76 | 77 | $parent.removeClass(ch) && $el.removeAttr(ch); 78 | }); 79 | 80 | $parent[checkAction ? 'addClass' : 'removeClass'](ch) && checkAction ? $el.attr(ch, true) : $el.removeAttr(ch); 81 | $el.trigger(e); 82 | 83 | if (checked !== $el.prop(ch)) { 84 | $el.trigger('change'); 85 | } 86 | } 87 | 88 | } 89 | 90 | 91 | /* RADIO PLUGIN DEFINITION 92 | * ======================== */ 93 | 94 | var old = $.fn.radio 95 | 96 | $.fn.radio = function (option) { 97 | return this.each(function () { 98 | var $this = $(this) 99 | , data = $this.data('radio') 100 | , options = $.extend({}, $.fn.radio.defaults, $this.data(), typeof option == 'object' && option); 101 | if (!data) $this.data('radio', (data = new Radio(this, options))); 102 | if (option == 'toggle') data.toggle() 103 | if (option == 'check' || option == 'uncheck') data.setCheck(option) 104 | else if (option) data.setState(); 105 | }); 106 | } 107 | 108 | $.fn.radio.defaults = { 109 | template: '' 110 | } 111 | 112 | 113 | /* RADIO NO CONFLICT 114 | * ================== */ 115 | 116 | $.fn.radio.noConflict = function () { 117 | $.fn.radio = old; 118 | return this; 119 | } 120 | 121 | 122 | /* RADIO DATA-API 123 | * =============== */ 124 | 125 | $(document).on('click.radio.data-api', '[data-toggle^=radio], .radio', function (e) { 126 | var $radio = $(e.target); 127 | e && e.preventDefault() && e.stopPropagation(); 128 | if (!$radio.hasClass('radio')) $radio = $radio.closest('.radio'); 129 | $radio.find(':radio').radio('toggle'); 130 | }); 131 | 132 | $(window).on('load', function () { 133 | $('[data-toggle="radio"]').each(function () { 134 | var $radio = $(this); 135 | $radio.radio(); 136 | }); 137 | }); 138 | 139 | }(window.jQuery); -------------------------------------------------------------------------------- /pyportify/pkcs1/primes.py: -------------------------------------------------------------------------------- 1 | import fractions 2 | from . import primitives 3 | 4 | from .defaults import default_pseudo_random, default_crypto_random 5 | 6 | PRIME_ALGO = 'miller-rabin' 7 | gmpy = None 8 | try: 9 | import gmpy 10 | PRIME_ALGO = 'gmpy-miller-rabin' 11 | except ImportError: 12 | pass 13 | 14 | DEFAULT_ITERATION = 1000 15 | 16 | USE_MILLER_RABIN = True 17 | 18 | 19 | def is_prime(n, rnd=default_pseudo_random, k=DEFAULT_ITERATION, 20 | algorithm=None): 21 | '''Test if n is a prime number 22 | 23 | m - the integer to test 24 | rnd - the random number generator to use for the probalistic primality 25 | algorithms, 26 | k - the number of iterations to use for the probabilistic primality 27 | algorithms, 28 | algorithm - the primality algorithm to use, default is Miller-Rabin. The 29 | gmpy implementation is used if gmpy is installed. 30 | 31 | Return value: True is n seems prime, False otherwise. 32 | ''' 33 | 34 | if algorithm is None: 35 | algorithm = PRIME_ALGO 36 | if algorithm == 'gmpy-miller-rabin': 37 | if not gmpy: 38 | raise NotImplementedError 39 | return gmpy.is_prime(n, k) 40 | elif algorithm == 'miller-rabin': 41 | # miller rabin probability of primality is 1/4**k 42 | return miller_rabin(n, k, rnd=rnd) 43 | elif algorithm == 'solovay-strassen': 44 | # for jacobi it's 1/2**k 45 | return randomized_primality_testing(n, rnd=rnd, k=k*2) 46 | else: 47 | raise NotImplementedError 48 | 49 | 50 | def get_prime(size=128, rnd=default_crypto_random, k=DEFAULT_ITERATION, 51 | algorithm=None): 52 | '''Generate a prime number of the giver size using the is_prime() helper 53 | function. 54 | 55 | size - size in bits of the prime, default to 128 56 | rnd - a random generator to use 57 | k - the number of iteration to use for the probabilistic primality 58 | algorithms. 59 | algorithm - the name of the primality algorithm to use, default is the 60 | probabilistic Miller-Rabin algorithm. 61 | 62 | Return value: a prime number, as a long integer 63 | ''' 64 | while True: 65 | n = rnd.getrandbits(size-2) 66 | n = 2 ** (size-1) + n * 2 + 1 67 | if is_prime(n, rnd=rnd, k=k, algorithm=algorithm): 68 | return n 69 | if algorithm == 'gmpy-miller-rabin': 70 | return gmpy.next_prime(n) 71 | 72 | 73 | def jacobi(a, b): 74 | '''Calculates the value of the Jacobi symbol (a/b) where both a and b are 75 | positive integers, and b is odd 76 | 77 | :returns: -1, 0 or 1 78 | ''' 79 | 80 | assert a > 0 81 | assert b > 0 82 | 83 | if a == 0: 84 | return 0 85 | result = 1 86 | while a > 1: 87 | if a & 1: 88 | if ((a-1)*(b-1) >> 2) & 1: 89 | result = -result 90 | a, b = b % a, a 91 | else: 92 | if (((b * b) - 1) >> 3) & 1: 93 | result = -result 94 | a >>= 1 95 | if a == 0: 96 | return 0 97 | return result 98 | 99 | 100 | def jacobi_witness(x, n): 101 | '''Returns False if n is an Euler pseudo-prime with base x, and 102 | True otherwise. 103 | ''' 104 | j = jacobi(x, n) % n 105 | f = pow(x, n >> 1, n) 106 | return j != f 107 | 108 | 109 | def randomized_primality_testing(n, rnd=default_crypto_random, 110 | k=DEFAULT_ITERATION): 111 | '''Calculates whether n is composite (which is always correct) or 112 | prime (which is incorrect with error probability 2**-k) 113 | 114 | Returns False if the number is composite, and True if it's 115 | probably prime. 116 | ''' 117 | 118 | # 50% of Jacobi-witnesses can report compositness of non-prime numbers 119 | 120 | # The implemented algorithm using the Jacobi witness function has error 121 | # probability q <= 0.5, according to Goodrich et. al 122 | # 123 | # q = 0.5 124 | # t = int(math.ceil(k / log(1 / q, 2))) 125 | # So t = k / log(2, 2) = k / 1 = k 126 | # this means we can use range(k) rather than range(t) 127 | 128 | for _ in range(k): 129 | x = rnd.randint(0, n-1) 130 | if jacobi_witness(x, n): 131 | return False 132 | return True 133 | 134 | 135 | def miller_rabin(n, k, rnd=default_pseudo_random): 136 | ''' 137 | Pure python implementation of the Miller-Rabin algorithm. 138 | 139 | n - the integer number to test, 140 | k - the number of iteration, the probability of n being prime if the 141 | algorithm returns True is 1/2**k, 142 | rnd - a random generator 143 | ''' 144 | s = 0 145 | d = n-1 146 | # Find nearest power of 2 147 | s = primitives.integer_bit_size(n) 148 | # Find greatest factor which is a power of 2 149 | s = fractions.gcd(2**s, n-1) 150 | d = (n-1) // s 151 | s = primitives.integer_bit_size(s) - 1 152 | while k: 153 | k = k - 1 154 | a = rnd.randint(2, n-2) 155 | x = pow(a, d, n) 156 | if x == 1 or x == n - 1: 157 | continue 158 | for r in range(1, s-1): 159 | x = pow(x, 2, n) 160 | if x == 1: 161 | return False 162 | if x == n - 1: 163 | break 164 | else: 165 | return False 166 | return True 167 | -------------------------------------------------------------------------------- /pyportify/pkcs1/keys.py: -------------------------------------------------------------------------------- 1 | import fractions 2 | from . import primitives 3 | from . import exceptions 4 | 5 | from .defaults import default_crypto_random 6 | from .primes import get_prime, DEFAULT_ITERATION 7 | 8 | 9 | class RsaPublicKey(object): 10 | __slots__ = ('n', 'e', 'bit_size', 'byte_size') 11 | 12 | def __init__(self, n, e): 13 | self.n = n 14 | self.e = e 15 | self.bit_size = primitives.integer_bit_size(n) 16 | self.byte_size = primitives.integer_byte_size(n) 17 | 18 | def __repr__(self): 19 | return '' % \ 20 | (self.n, self.e, self.bit_size) 21 | 22 | def rsavp1(self, s): 23 | if not (0 <= s <= self.n-1): 24 | raise exceptions.SignatureRepresentativeOutOfRange 25 | return self.rsaep(s) 26 | 27 | def rsaep(self, m): 28 | if not (0 <= m <= self.n-1): 29 | raise exceptions.MessageRepresentativeOutOfRange 30 | return pow(m, self.e, self.n) 31 | 32 | 33 | class RsaPrivateKey(object): 34 | __slots__ = ('n', 'd', 'bit_size', 'byte_size') 35 | 36 | def __init__(self, n, d): 37 | self.n = n 38 | self.d = d 39 | self.bit_size = primitives.integer_bit_size(n) 40 | self.byte_size = primitives.integer_byte_size(n) 41 | 42 | def __repr__(self): 43 | return '' % \ 44 | (self.n, self.d, self.bit_size) 45 | 46 | def rsadp(self, c): 47 | if not (0 <= c <= self.n-1): 48 | raise exceptions.CiphertextRepresentativeOutOfRange 49 | return pow(c, self.d, self.n) 50 | 51 | def rsasp1(self, m): 52 | if not (0 <= m <= self.n-1): 53 | raise exceptions.MessageRepresentativeOutOfRange 54 | return self.rsadp(m) 55 | 56 | 57 | class MultiPrimeRsaPrivateKey(object): 58 | __slots__ = ('primes', 'blind', 'blind_inv', 'n', 'e', 'exponents', 'crts', 59 | 'bit_size', 'byte_size') 60 | 61 | def __init__(self, primes, e, blind=True, rnd=default_crypto_random): 62 | self.primes = primes 63 | self.n = primitives.product(*primes) 64 | self.e = e 65 | self.bit_size = primitives.integer_bit_size(self.n) 66 | self.byte_size = primitives.integer_byte_size(self.n) 67 | self.exponents = [] 68 | for prime in primes: 69 | exponent, a, b = primitives.bezout(e, prime-1) 70 | assert b == 1 71 | if exponent < 0: 72 | exponent += prime-1 73 | self.exponents.append(exponent) 74 | self.crts = [1] 75 | R = primes[0] 76 | for prime in primes[1:]: 77 | crt, a, b = primitives.bezout(R, prime) 78 | assert b == 1 79 | R *= prime 80 | self.crts.append(crt) 81 | public = RsaPublicKey(self.n, self.e) 82 | if blind: 83 | while True: 84 | blind_factor = rnd.getrandbits(self.bit_size-1) 85 | self.blind = public.rsaep(blind_factor) 86 | u, v, gcd = primitives.bezout(blind_factor, self.n) 87 | if gcd == 1: 88 | self.blind_inv = u if u > 0 else u + self.n 89 | assert (blind_factor * self.blind_inv) % self.n == 1 90 | break 91 | else: 92 | self.blind = None 93 | self.blind_inv = None 94 | 95 | def __repr__(self): 96 | return '' % \ 97 | (self.n, self.primes, self.bit_size) 98 | 99 | def rsadp(self, c): 100 | if not (0 <= c <= self.n-1): 101 | raise exceptions.CiphertextRepresentativeOutOfRange 102 | R = 1 103 | m = 0 104 | if self.blind: 105 | c = (c * self.blind) % self.n 106 | contents = zip(self.primes, self.exponents, self.crts) 107 | for prime, exponent, crt in contents: 108 | m_i = primitives._pow(c, exponent, prime) 109 | h = ((m_i - m) * crt) % prime 110 | m += R * h 111 | R *= prime 112 | if self.blind_inv: 113 | m = (m * self.blind_inv) % self.n 114 | return m 115 | 116 | def rsasp1(self, m): 117 | if not (0 <= m <= self.n-1): 118 | raise exceptions.MessageRepresentativeOutOfRange 119 | return self.rsadp(m) 120 | 121 | 122 | def generate_key_pair(size=512, number=2, rnd=default_crypto_random, 123 | k=DEFAULT_ITERATION, primality_algorithm=None, 124 | strict_size=True, e=0x10001): 125 | '''Generates an RSA key pair. 126 | 127 | size: 128 | the bit size of the modulus, default to 512. 129 | number: 130 | the number of primes to use, default to 2. 131 | rnd: 132 | the random number generator to use, default to SystemRandom from the 133 | random library. 134 | k: 135 | the number of iteration to use for the probabilistic primality 136 | tests. 137 | primality_algorithm: 138 | the primality algorithm to use. 139 | strict_size: 140 | whether to use size as a lower bound or a strict goal. 141 | e: 142 | the public key exponent. 143 | 144 | Returns the pair (public_key, private_key). 145 | ''' 146 | primes = [] 147 | lbda = 1 148 | bits = size // number + 1 149 | n = 1 150 | while len(primes) < number: 151 | if number - len(primes) == 1: 152 | bits = size - primitives.integer_bit_size(n) + 1 153 | prime = get_prime(bits, rnd, k, algorithm=primality_algorithm) 154 | if prime in primes: 155 | continue 156 | if e is not None and fractions.gcd(e, lbda) != 1: 157 | continue 158 | if (strict_size and number - len(primes) == 1 and 159 | primitives.integer_bit_size(n*prime) != size): 160 | continue 161 | primes.append(prime) 162 | n *= prime 163 | lbda *= prime - 1 164 | if e is None: 165 | e = 0x10001 166 | while e < lbda: 167 | if fractions.gcd(e, lbda) == 1: 168 | break 169 | e += 2 170 | assert 3 <= e <= n-1 171 | public = RsaPublicKey(n, e) 172 | private = MultiPrimeRsaPrivateKey(primes, e, blind=True, rnd=rnd) 173 | return public, private 174 | -------------------------------------------------------------------------------- /pyportify/google.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | import urllib 4 | from uuid import getnode as getmac 5 | 6 | from pyportify import gpsoauth 7 | 8 | SJ_DOMAIN = "mclients.googleapis.com" 9 | SJ_URL = "/sj/v2.5" 10 | 11 | FULL_SJ_URL = "https://{0}{1}".format(SJ_DOMAIN, SJ_URL) 12 | 13 | 14 | def encode(values): 15 | return urllib.parse.urlencode(values) 16 | 17 | 18 | class Mobileclient(object): 19 | 20 | def __init__(self, session, token=None): 21 | self.token = token 22 | self.session = session 23 | 24 | async def login(self, username, password): 25 | android_id = _get_android_id() 26 | res = gpsoauth.perform_master_login(username, password, android_id) 27 | 28 | if "Token" not in res: 29 | return None 30 | 31 | self._master_token = res['Token'] 32 | res = gpsoauth.perform_oauth( 33 | username, self._master_token, android_id, 34 | service='sj', app='com.google.android.music', 35 | client_sig='38918a453d07199354f8b19af05ec6562ced5788') 36 | if 'Auth' not in res: 37 | return None 38 | self.token = res["Auth"] 39 | return self.token 40 | 41 | async def search_all_access(self, search_query, max_results=30): 42 | data = await self._http_get("/query", {"q": search_query, 43 | "max-results": max_results, 44 | "ct": "1,2,3,4,6,7,8,9"}) 45 | return data 46 | 47 | async def find_best_tracks(self, search_query): 48 | tracks = [] 49 | for i in range(0, 2): 50 | data = await self.search_all_access(search_query) 51 | if 'suggestedQuery' in data: 52 | data = await self.search_all_access( 53 | data['suggestedQuery']) 54 | if "entries" not in data: 55 | continue 56 | for entry in data["entries"]: 57 | if entry["type"] == "1": 58 | tracks.append(entry["track"]) 59 | if tracks: 60 | break 61 | return tracks 62 | 63 | async def fetch_playlists(self): 64 | data = await self._http_post("/playlistfeed", {}) 65 | # TODO: paging 66 | return data 67 | 68 | async def create_playlist(self, name, public=False): 69 | mutations = build_create_playlist(name, public) 70 | data = await self._http_post("/playlistbatch", 71 | {"mutations": mutations}) 72 | res = data["mutate_response"] 73 | playlist_id = res[0]["id"] 74 | return playlist_id 75 | 76 | async def add_songs_to_playlist(self, playlist_id, track_ids): 77 | data = {"mutations": build_add_tracks(playlist_id, track_ids)} 78 | res = await self._http_post('/plentriesbatch', data) 79 | added_ids = [e['id'] for e in res['mutate_response']] 80 | return added_ids 81 | 82 | async def _http_get(self, url, params): 83 | headers = {"Authorization": "GoogleLogin auth={0}".format(self.token), 84 | "Content-type": "application/json"} 85 | 86 | merged_params = params.copy() 87 | merged_params.update({'tier': 'aa', 88 | 'hl': 'en_US', 89 | 'dv': 0}) 90 | 91 | res = await self.session.request('GET', 92 | FULL_SJ_URL + url, 93 | headers=headers, 94 | params=merged_params) 95 | data = await res.json() 96 | return data 97 | 98 | async def _http_post(self, url, data): 99 | data = json.dumps(data) 100 | headers = {"Authorization": "GoogleLogin auth={0}".format(self.token), 101 | "Content-type": "application/json"} 102 | res = await self.session.request( 103 | 'POST', 104 | FULL_SJ_URL + url, 105 | data=data, 106 | headers=headers, 107 | params={'tier': 'aa', 108 | 'hl': 'en_US', 109 | 'dv': 0, 110 | 'alt': 'json'}) 111 | ret = await res.json() 112 | return ret 113 | 114 | 115 | def build_add_tracks(playlist_id, track_ids): 116 | mutations = [] 117 | prev_id = "" 118 | cur_id = str(uuid.uuid1()) 119 | next_id = str(uuid.uuid1()) 120 | 121 | for i, track_id in enumerate(track_ids): 122 | details = {"create": {"clientId": cur_id, 123 | "creationTimestamp": "-1", 124 | "deleted": False, 125 | "lastModifiedTimestamp": "0", 126 | "playlistId": playlist_id, 127 | "source": 1, 128 | "trackId": track_id}} 129 | 130 | if track_id.startswith("T"): 131 | details["create"]["source"] = 2 # AA track 132 | 133 | if i > 0: 134 | details["create"]["precedingEntryId"] = prev_id 135 | 136 | if i < len(track_ids) - 1: 137 | details["create"]["followingEntryId"] = next_id 138 | 139 | mutations.append(details) 140 | 141 | prev_id = cur_id 142 | cur_id = next_id 143 | next_id = str(uuid.uuid1()) 144 | return mutations 145 | 146 | 147 | def build_create_playlist(name, public): 148 | return [{ 149 | "create": {"creationTimestamp": "-1", 150 | "deleted": False, 151 | "lastModifiedTimestamp": 0, 152 | "name": name, 153 | "description": "", 154 | "type": "USER_GENERATED", 155 | "shareState": "PUBLIC" if public else "PRIVATE"}}] 156 | 157 | 158 | def parse_auth_response(s): 159 | # SID=DQAAAGgA...7Zg8CTN 160 | # LSID=DQAAAGsA...lk8BBbG 161 | # Auth=DQAAAGgA...dk3fA5N 162 | res = {} 163 | for line in s.split("\n"): 164 | if not line: 165 | continue 166 | k, v = line.split("=", 1) 167 | res[k] = v 168 | return res 169 | 170 | 171 | def _get_android_id(): 172 | mac_int = getmac() 173 | if (mac_int >> 40) % 2: 174 | raise OSError("a valid MAC could not be determined." 175 | " Provide an android_id (and be" 176 | " sure to provide the same one on future runs).") 177 | 178 | android_id = _create_mac_string(mac_int) 179 | android_id = android_id.replace(':', '') 180 | return android_id 181 | 182 | 183 | def _create_mac_string(num, splitter=':'): 184 | mac = hex(num)[2:] 185 | if mac[-1] == 'L': 186 | mac = mac[:-1] 187 | pad = max(12 - len(mac), 0) 188 | mac = '0' * pad + mac 189 | mac = splitter.join([mac[x:x + 2] for x in range(0, 12, 2)]) 190 | mac = mac.upper() 191 | return mac 192 | -------------------------------------------------------------------------------- /pyportify/static/js/controllers.js: -------------------------------------------------------------------------------- 1 | function MainCtrl($scope, $route, $routeParams, $location, context) { 2 | $scope.context = context.items(); 3 | $scope.app_name = "Pyportify"; 4 | } 5 | 6 | function WelcomeCtrl($scope, $rootScope, $route, $routeParams, $location) { 7 | $rootScope.step = 0; 8 | $rootScope.link = 'About Pyportify'; 9 | } 10 | 11 | function AboutCtrl($scope, $rootScope, $route, $routeParams, $location) { 12 | $rootScope.link = '« Back'; 13 | } 14 | 15 | function ProcessTransferCtrl($scope, $rootScope, $filter, $http, $route, $routeParams, $location, socket, context, portifyService, $timeout, $anchorScroll) { 16 | $rootScope.step = 4; 17 | $rootScope.link = ''; 18 | $scope.playlists = context.items(); 19 | $scope.alldone = false; 20 | $scope.processing = false; 21 | 22 | $scope.notfound = []; 23 | $scope.shownotfound = false; 24 | 25 | $scope.currentPlaylist = { 26 | name: "", 27 | processed: 0, 28 | found: 0, 29 | notfound: 0, 30 | karaoke: 0, 31 | count: 0, 32 | progress: 0, 33 | }; 34 | 35 | $timeout(function() { 36 | portifyService.startTransfer($scope.playlists); 37 | }, 600); 38 | 39 | $scope.hideMissing = function() { 40 | $scope.shownotfound = false; 41 | }; 42 | 43 | $scope.showMissing = function() { 44 | $scope.shownotfound = true; 45 | }; 46 | 47 | socket.on('portify', function (data) { 48 | data = JSON.parse(data.data)['eventData'] 49 | if(data.type == "playlist_started") { 50 | $scope.cover = null; 51 | $scope.playlist = data.data.playlist.name; 52 | $scope.status = "Transfering..."+$scope.playlist; 53 | $scope.tracks = []; 54 | $scope.currentPlaylist = { 55 | name: data.data.playlist.name, 56 | processed: 0, 57 | found: 0, 58 | notfound: 0, 59 | karaoke: 0, 60 | count: 0, 61 | progress: 0 62 | }; 63 | $scope.processing = true; 64 | } else if(data.type == "all_done") { 65 | $scope.alldone = true; 66 | } else if(data.type == "playlist_done") { 67 | $scope.processing = false; 68 | } else if(data.type = "playlist_length") { 69 | $scope.currentPlaylist.count = data.data.length; 70 | } 71 | }); 72 | 73 | socket.on('gmusic', function (data) { 74 | data = JSON.parse(data.data)['eventData'] 75 | if(data.type == "added") { 76 | $scope.currentPlaylist.processed++; 77 | $scope.currentPlaylist.found++; 78 | } else if(data.type == "not_added") { 79 | $scope.notfound.push({"name": data.data.spotify_track_name}); 80 | $scope.currentPlaylist.processed++; 81 | $scope.currentPlaylist.notfound++; 82 | if(data.data.karaoke) { 83 | $scope.currentPlaylist.karaoke++; 84 | } 85 | } 86 | if($scope.currentPlaylist.count == 0) 87 | $scope.currentPlaylist.progress = "0%"; 88 | else 89 | $scope.currentPlaylist.progress = (($scope.currentPlaylist.processed / $scope.currentPlaylist.count)*100) +"%"; 90 | }); 91 | } 92 | 93 | function FancyProcessTransferCtrl($scope, $rootScope, $filter, $http, $route, $routeParams, $location, socket, context, portifyService, $timeout, $anchorScroll) { 94 | $rootScope.step = 4; 95 | $rootScope.link = ''; 96 | $scope.playlists = context.items(); 97 | $scope.tracks = []; 98 | $scope.playlistsDone = 0; 99 | $scope.alldone = false; 100 | $scope.ttracks = []; 101 | portifyService.startTransfer($scope.playlists); 102 | 103 | 104 | function findIndexByKeyValue(obj, key, value) { 105 | for (var i = 0; i < obj.length; i++) { 106 | if (obj[i][key] == value) { 107 | return i; 108 | } 109 | } 110 | return null; 111 | } 112 | 113 | socket.on('portify', function (data) { 114 | data = JSON.parse(data.data)['eventData'] 115 | if(data.type == "playlist_started") { 116 | $scope.playlist = data.data.playlist.name; 117 | $scope.status = "Transfering..."+$scope.playlist; 118 | $scope.tracks = []; 119 | } else if(data.type == "all_done") { 120 | $scope.alldone = true; 121 | } 122 | }); 123 | 124 | socket.on('gmusic', function (data) { 125 | data = JSON.parse(data.data)['eventData'] 126 | var myidx = findIndexByKeyValue($scope.tracks, "id", data.data.spotify_track_uri ); 127 | if(data.type == "found_possible_matches") { 128 | 129 | if(data.data.found) { 130 | //$scope.tracks[data.data.spotify_track_uri].class.push('color'); 131 | $scope.tracks[myidx].class.push('color'); 132 | } else { 133 | //$scope.tracks[data.data.spotify_track_uri].nok = true; 134 | $scope.tracks[myidx].nok = true; 135 | } 136 | } else if(data.type == "added") { 137 | $scope.tracks[myidx].ok = true; 138 | } 139 | }); 140 | 141 | socket.on('spotify', function (data) { 142 | if(data.type == "track") { 143 | var image = new Image(); 144 | image.src = data.data.cover; 145 | image.onload = function(){ 146 | $scope.tracks.push({ id: data.data.spotify_track_uri, src: data.data.cover, class: ['album', '']}); 147 | $scope.ttracks = $filter('limitTo')($scope.tracks,-60); 148 | } 149 | } 150 | }); 151 | } 152 | 153 | function GoogleLoginCtrl($scope, $rootScope, $http, $location, socket) { 154 | $rootScope.step = 1; 155 | $rootScope.link = ''; 156 | $scope.googleLogin = function() { 157 | $http({ 158 | url: "/google/login", 159 | dataType: "json", 160 | method: "POST", 161 | data: $scope.loginData, 162 | headers: { 163 | "Content-Type": "application/json; charset=utf-8" 164 | } 165 | }).success(function(response){ 166 | if(response.status == 200) { 167 | $location.path( "/spotify/login" ); 168 | } else { 169 | alert("Login failed."); 170 | } 171 | }).error(function(error){ 172 | $scope.error = error; 173 | }); 174 | }; 175 | socket.on('test', function (data) { 176 | console.log(data); 177 | }); 178 | } 179 | 180 | function SpotifyLoginCtrl($scope, $rootScope, $http, $location) { 181 | $rootScope.step = 2; 182 | $rootScope.link = ''; 183 | $scope.spotifyLogin = function() { 184 | $http({ 185 | url: "/spotify/login", 186 | dataType: "json", 187 | method: "POST", 188 | data: $scope.loginData, 189 | headers: { 190 | "Content-Type": "application/json; charset=utf-8" 191 | } 192 | }).success(function(response){ 193 | if(response.status == 200) { 194 | $location.path( "/spotify/playlists/select" ); 195 | } else { 196 | alert("Login failed."); 197 | } 198 | }).error(function(error){ 199 | $scope.error = error; 200 | }); 201 | }; 202 | } 203 | 204 | function SelectSpotifyCtrl($scope, $rootScope, $http, $location, portifyService, context) { 205 | $scope.playlists = portifyService.getSpotifyPlaylists(); 206 | $rootScope.step = 3; 207 | $rootScope.link = ''; 208 | $scope.selectAll = function ($event){ 209 | var checkbox = $event.target; 210 | for ( var i = 0; i < $scope.playlists.$$v.length; i++) { 211 | $scope.playlists.$$v[i].transfer = checkbox.checked; 212 | } 213 | }; 214 | 215 | $scope.startTransfer = function() { 216 | context.clear(); 217 | for ( var i = 0; i < $scope.playlists.$$v.length; i++) { 218 | if($scope.playlists.$$v[i].transfer) { 219 | context.addItem($scope.playlists.$$v[i]); 220 | } 221 | } 222 | 223 | if(context.items().length == 0) 224 | alert("Please select at least one playlist"); 225 | else 226 | $location.path( "/transfer/process" ); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /pyportify/static/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | padding: 0px; 4 | } 5 | 6 | .container { 7 | margin: 0px; 8 | padding: 0px; 9 | width: auto; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | bottom: 60px; 15 | } 16 | 17 | .additional_bottom { 18 | margin-top: 10px; 19 | margin-bottom: 10px; 20 | } 21 | 22 | .container div.card { 23 | position: absolute; 24 | left: 0; 25 | right: 0; 26 | bottom: 24px; 27 | top: 0; 28 | overflow-y: auto; 29 | } 30 | 31 | .row { 32 | margin-left: auto; 33 | } 34 | 35 | .bottom_bar { 36 | z-index: 5; 37 | position: absolute; 38 | bottom: 0px; 39 | height: 60px; 40 | left: 0px; 41 | right: 0px; 42 | background: #7F8C8D; 43 | border-top: 4px solid #ffffff; 44 | padding: 10px; 45 | } 46 | 47 | .card { 48 | padding: 10px; 49 | } 50 | 51 | .portify { 52 | background: #E67E22; 53 | color: #ffffff; 54 | } 55 | 56 | .process_top { 57 | position: absolute; 58 | top: 0px; 59 | left: 0px; 60 | right: 0px; 61 | height: 30px; 62 | background: rgba(133,133,133,0.6); 63 | opacity: 0.6; 64 | z-index: 9; 65 | color: #ffffff; 66 | font-weight: bold; 67 | font-size: 13pt; 68 | line-height: 25px; 69 | padding-left: 5px; 70 | } 71 | 72 | .notfound { 73 | position: absolute; 74 | top: 0px; 75 | left: 0px; 76 | right: 0px; 77 | bottom: 23px; 78 | background: rgba(133,133,133,1); 79 | color: #ffffff; 80 | opacity: 1; 81 | z-index: 16; 82 | padding: 14px; 83 | overflow-y: auto; 84 | } 85 | 86 | .notfound a { 87 | color: #ffffff; 88 | } 89 | 90 | .done { 91 | position: absolute; 92 | top: 0px; 93 | left: 0px; 94 | right: 0px; 95 | bottom: 0px; 96 | background: rgba(133,133,133,0.8); 97 | opacity: 1; 98 | z-index: 15; 99 | color: #ffffff; 100 | font-weight: bold; 101 | font-size: 13pt; 102 | text-align: center; 103 | } 104 | 105 | .done h1 { 106 | margin-top: 25%; 107 | } 108 | .done a { 109 | color: #FFFFFF; 110 | } 111 | 112 | 113 | .ok,.nok { 114 | position: absolute; z-index: 5; right: 10px; bottom: 0px; 115 | display: block; 116 | } 117 | 118 | .ok,.nok { 119 | font-size: 300%; 120 | opacity: 0.6; 121 | } 122 | 123 | .nok { 124 | z-index: 6; 125 | } 126 | 127 | /* 128 | .process div { 129 | -webkit-filter: grayscale(1); 130 | -webkit-transition:-webkit-filter 0.8s ease-in-out; 131 | }*/ 132 | 133 | .album { 134 | width: 10%; 135 | float: left; 136 | position: relative; 137 | } 138 | 139 | .process div.color { 140 | -webkit-filter: grayscale(0); 141 | } 142 | 143 | .view-anim-leave { 144 | -webkit-transition: .5s linear opacity; 145 | opacity: 1; 146 | } 147 | 148 | .view-anim-leave.view-anim-leave-active { 149 | opacity: 0; 150 | } 151 | 152 | .view-anim-enter, done-anim-enter { 153 | -webkit-transition: .5s linear opacity; 154 | 155 | opacity: 0; 156 | } 157 | 158 | .view-anim-enter.view-anim-enter-active, .done-anim-enter.done-anim-enter-active { 159 | opacity: 1; 160 | } 161 | 162 | .row { 163 | right: 0px; 164 | left: 0px; 165 | } 166 | 167 | .col { 168 | position: relative; 169 | width: 10%; 170 | padding-bottom: 10%; 171 | float: left; 172 | height: 0; 173 | box-sizing: border-box; 174 | } 175 | 176 | .current_playlist { 177 | position: absolute; 178 | left: 5%; 179 | right: 5%; 180 | top: 50%; 181 | height: 200px; 182 | background: #7F8C8D; 183 | z-index: 100; 184 | margin-top: -100px; 185 | } 186 | 187 | .current_playlist .progress .bar { 188 | background: #E67E22; 189 | } 190 | .current_playlist .progress { 191 | border-radius: 0px; 192 | margin-bottom: 5px; 193 | } 194 | 195 | .process_details { 196 | padding: 0px 10px 0px 10px; 197 | } 198 | 199 | .process_details h1 { 200 | margin-bottom: 20px; 201 | } 202 | 203 | .process_details { 204 | font-size: 205%; 205 | text-align: center; 206 | } 207 | 208 | 209 | .simpleprocess-anim-enter { 210 | -webkit-animation-duration: 1s; 211 | -webkit-animation-delay: .5s; 212 | -webkit-animation-timing-function: ease; 213 | -webkit-animation-fill-mode: both; 214 | opacity: 0; 215 | } 216 | 217 | .simpleprocess-anim-enter.simpleprocess-anim-enter-active { 218 | -webkit-animation-name: bounceInDown; 219 | opacity: 1; 220 | } 221 | 222 | .simpleprocess-anim-leave { 223 | -webkit-animation-duration: 1s; 224 | -webkit-animation-delay: .5s; 225 | -webkit-animation-timing-function: ease; 226 | -webkit-animation-fill-mode: both; 227 | opacity: 1; 228 | } 229 | 230 | .simpleprocess-anim-leave.simpleprocess-anim-leave-active { 231 | -webkit-animation-name: bounceOutDown; 232 | } 233 | 234 | 235 | .album-anim-enter { 236 | -webkit-animation-duration: .8s; 237 | -webkit-animation-delay: .1s; 238 | -webkit-animation-timing-function: ease; 239 | -webkit-animation-fill-mode: both; 240 | opacity: 0; 241 | } 242 | 243 | .album-anim-enter.album-anim-enter-active { 244 | -webkit-animation-name: fadeIn; 245 | opacity: 1; 246 | } 247 | 248 | .album-anim-leave { 249 | -webkit-animation-duration: .5s; 250 | -webkit-animation-delay: 0s; 251 | -webkit-animation-timing-function: ease; 252 | opacity: 1; 253 | } 254 | 255 | .album-anim-leave.album-anim-leave-active { 256 | -webkit-animation-name: fadeOut; 257 | } 258 | 259 | .placeholder { 260 | background: url('../img/no_album.png'); 261 | } 262 | 263 | .container div.process { 264 | padding: 0px; 265 | margin: 0px; 266 | overflow-y: hidden; 267 | -webkit-user-select: none; 268 | background-image: url('/img/big_cover.png'); 269 | -webkit-transition:-webkit-filter 0.8s ease-in-out, background 1s linear; 270 | 271 | background-size: cover !important; 272 | background-repeat: no-repeat !important; 273 | } 274 | 275 | .blur { 276 | -webkit-filter: blur(5px); 277 | } 278 | 279 | .album img { 280 | width: 100%; 281 | } 282 | 283 | .portify a { 284 | color: #ffffff; 285 | } 286 | 287 | .google { 288 | background: #2980B9; 289 | color: #ffffff; 290 | } 291 | 292 | .google legend { 293 | color: #ffffff; 294 | } 295 | 296 | .spotify { 297 | background: #9cca3b; 298 | color: #ffffff; 299 | } 300 | 301 | .spotify table { 302 | background: #ECF0F1; 303 | } 304 | 305 | .spotify thead { 306 | background: #BDC3C7; 307 | } 308 | 309 | .spotify tbody { 310 | color: #7F8C8D; 311 | } 312 | 313 | .spotify legend { 314 | color: #ffffff; 315 | } 316 | 317 | .test { 318 | -webkit-animation-duration: 1s; 319 | -webkit-animation-delay: .2s; 320 | -webkit-animation-timing-function: ease; 321 | -webkit-animation-fill-mode: both; 322 | } 323 | 324 | fieldset .btn { 325 | margin-bottom: 10px; 326 | } 327 | 328 | .intro_text { 329 | float: left; 330 | width: 50%; 331 | font-size: 16pt; 332 | } 333 | 334 | .intro_logo { 335 | float: right; 336 | width: 50%; 337 | text-align: right; 338 | } 339 | 340 | .bottom_bar .btn.btn-primary { 341 | background-color: #BDC3C7; 342 | } 343 | 344 | .bottom_bar .btn.btn-primary:hover, .btn.btn-primary:focus, .btn-group:focus .btn.btn-primary.dropdown-toggle { 345 | background-color: #95A5A6; 346 | } 347 | 348 | .bottom_bar .btn.btn-primary:active, .btn-group.open .btn.btn-primary.dropdown-toggle, .btn.btn-primary.active { 349 | background-color: #E67E22; 350 | } 351 | 352 | .pagination { 353 | margin: 10px 0; 354 | } 355 | 356 | ::-webkit-scrollbar { 357 | height: 12px; 358 | width: 12px; 359 | } 360 | ::-webkit-scrollbar-thumb { 361 | background: rgba(0, 0, 0, 0.35); 362 | -webkit-border-radius: 1ex; 363 | -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.55); 364 | } 365 | ::-webkit-scrollbar-corner { 366 | background: #000; 367 | } -------------------------------------------------------------------------------- /pyportify/static/js/imagesloaded.pkgd.min.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * imagesLoaded PACKAGED v3.0.1 4 | * JavaScript is all like "You images are done yet or what?" 5 | */ 6 | 7 | (function(e) { 8 | "use strict"; 9 | 10 | function t() {} 11 | function n(e, t) { 12 | if (r) return t.indexOf(e); 13 | for (var n = t.length; n--;) if (t[n] === e) return n; 14 | return -1 15 | } 16 | var i = t.prototype, 17 | r = Array.prototype.indexOf ? !0 : !1; 18 | i._getEvents = function() { 19 | return this._events || (this._events = {}) 20 | }, i.getListeners = function(e) { 21 | var t, n, i = this._getEvents(); 22 | if ("object" == typeof e) { 23 | t = {}; 24 | for (n in i) i.hasOwnProperty(n) && e.test(n) && (t[n] = i[n]) 25 | } else t = i[e] || (i[e] = []); 26 | return t 27 | }, i.getListenersAsObject = function(e) { 28 | var t, n = this.getListeners(e); 29 | return n instanceof Array && (t = {}, t[e] = n), t || n 30 | }, i.addListener = function(e, t) { 31 | var i, r = this.getListenersAsObject(e); 32 | for (i in r) r.hasOwnProperty(i) && -1 === n(t, r[i]) && r[i].push(t); 33 | return this 34 | }, i.on = i.addListener, i.defineEvent = function(e) { 35 | return this.getListeners(e), this 36 | }, i.defineEvents = function(e) { 37 | for (var t = 0; e.length > t; t += 1) this.defineEvent(e[t]); 38 | return this 39 | }, i.removeListener = function(e, t) { 40 | var i, r, s = this.getListenersAsObject(e); 41 | for (r in s) s.hasOwnProperty(r) && (i = n(t, s[r]), - 1 !== i && s[r].splice(i, 1)); 42 | return this 43 | }, i.off = i.removeListener, i.addListeners = function(e, t) { 44 | return this.manipulateListeners(!1, e, t) 45 | }, i.removeListeners = function(e, t) { 46 | return this.manipulateListeners(!0, e, t) 47 | }, i.manipulateListeners = function(e, t, n) { 48 | var i, r, s = e ? this.removeListener : this.addListener, 49 | o = e ? this.removeListeners : this.addListeners; 50 | if ("object" != typeof t || t instanceof RegExp) for (i = n.length; i--;) s.call(this, t, n[i]); 51 | else for (i in t) t.hasOwnProperty(i) && (r = t[i]) && ("function" == typeof r ? s.call(this, i, r) : o.call(this, i, r)); 52 | return this 53 | }, i.removeEvent = function(e) { 54 | var t, n = typeof e, 55 | i = this._getEvents(); 56 | if ("string" === n) delete i[e]; 57 | else if ("object" === n) for (t in i) i.hasOwnProperty(t) && e.test(t) && delete i[t]; 58 | else delete this._events; 59 | return this 60 | }, i.emitEvent = function(e, t) { 61 | var n, i, r, s = this.getListenersAsObject(e); 62 | for (i in s) if (s.hasOwnProperty(i)) for (n = s[i].length; n--;) r = t ? s[i][n].apply(null, t) : s[i][n](), r === !0 && this.removeListener(e, s[i][n]); 63 | return this 64 | }, i.trigger = i.emitEvent, i.emit = function(e) { 65 | var t = Array.prototype.slice.call(arguments, 1); 66 | return this.emitEvent(e, t) 67 | }, "function" == typeof define && define.amd ? define(function() { 68 | return t 69 | }) : e.EventEmitter = t 70 | })(this), 71 | function(e) { 72 | "use strict"; 73 | var t = document.documentElement, 74 | n = function() {}; 75 | t.addEventListener ? n = function(e, t, n) { 76 | e.addEventListener(t, n, !1) 77 | } : t.attachEvent && (n = function(t, n, i) { 78 | t[n + i] = i.handleEvent ? function() { 79 | var t = e.event; 80 | t.target = t.target || t.srcElement, i.handleEvent.call(i, t) 81 | } : function() { 82 | var n = e.event; 83 | n.target = n.target || n.srcElement, i.call(t, n) 84 | }, t.attachEvent("on" + n, t[n + i]) 85 | }); 86 | var i = function() {}; 87 | t.removeEventListener ? i = function(e, t, n) { 88 | e.removeEventListener(t, n, !1) 89 | } : t.detachEvent && (i = function(e, t, n) { 90 | e.detachEvent("on" + t, e[t + n]); 91 | try { 92 | delete e[t + n] 93 | } catch (i) { 94 | e[t + n] = void 0 95 | } 96 | }); 97 | var r = { 98 | bind: n, 99 | unbind: i 100 | }; 101 | "function" == typeof define && define.amd ? define(r) : e.eventie = r 102 | }(this), 103 | function(e) { 104 | "use strict"; 105 | 106 | function t(e, t) { 107 | for (var n in t) e[n] = t[n]; 108 | return e 109 | } 110 | function n(e) { 111 | return "[object Array]" === a.call(e) 112 | } 113 | function i(e) { 114 | var t = []; 115 | if (n(e)) t = e; 116 | else if ("number" == typeof e.length) for (var i = 0, r = e.length; r > i; i++) t.push(e[i]); 117 | else t.push(e); 118 | return t 119 | } 120 | function r(e, n) { 121 | function r(e, n, o) { 122 | if (!(this instanceof r)) return new r(e, n); 123 | "string" == typeof e && (e = document.querySelectorAll(e)), this.elements = i(e), this.options = t({}, this.options), "function" == typeof n ? o = n : t(this.options, n), o && this.on("always", o), this.getImages(), s && (this.jqDeferred = new s.Deferred); 124 | var h = this; 125 | setTimeout(function() { 126 | h.check() 127 | }) 128 | } 129 | function a(e) { 130 | this.img = e 131 | } 132 | r.prototype = new e, r.prototype.options = {}, r.prototype.getImages = function() { 133 | this.images = []; 134 | for (var e = 0, t = this.elements.length; t > e; e++) { 135 | var n = this.elements[e]; 136 | "IMG" === n.nodeName && this.addImage(n); 137 | for (var i = n.querySelectorAll("img"), r = 0, s = i.length; s > r; r++) { 138 | var o = i[r]; 139 | this.addImage(o) 140 | } 141 | } 142 | }, r.prototype.addImage = function(e) { 143 | var t = new a(e); 144 | this.images.push(t) 145 | }, r.prototype.check = function() { 146 | function e(e, r) { 147 | return t.options.debug && h && o.log("confirm", e, r), t.progress(e), n++, n === i && t.complete(), !0 148 | } 149 | var t = this, 150 | n = 0, 151 | i = this.images.length; 152 | this.hasAnyBroken = !1; 153 | for (var r = 0; i > r; r++) { 154 | var s = this.images[r]; 155 | s.on("confirm", e), s.check() 156 | } 157 | }, r.prototype.progress = function(e) { 158 | this.hasAnyBroken = this.hasAnyBroken || !e.isLoaded, this.emit("progress", this, e), this.jqDeferred && this.jqDeferred.notify(this, e) 159 | }, r.prototype.complete = function() { 160 | var e = this.hasAnyBroken ? "fail" : "done"; 161 | if (this.isComplete = !0, this.emit(e, this), this.emit("always", this), this.jqDeferred) { 162 | var t = this.hasAnyBroken ? "reject" : "resolve"; 163 | this.jqDeferred[t](this) 164 | } 165 | }, s && (s.fn.imagesLoaded = function(e, t) { 166 | var n = new r(this, e, t); 167 | return n.jqDeferred.promise(s(this)) 168 | }); 169 | var f = {}; 170 | return a.prototype = new e, a.prototype.check = function() { 171 | var e = f[this.img.src]; 172 | if (e) return this.useCached(e), void 0; 173 | if (f[this.img.src] = this, this.img.complete && void 0 !== this.img.naturalWidth) return this.confirm(0 !== this.img.naturalWidth, "naturalWidth"), void 0; 174 | var t = this.proxyImage = new Image; 175 | n.bind(t, "load", this), n.bind(t, "error", this), t.src = this.img.src 176 | }, a.prototype.useCached = function(e) { 177 | if (e.isConfirmed) this.confirm(e.isLoaded, "cached was confirmed"); 178 | else { 179 | var t = this; 180 | e.on("confirm", function(e) { 181 | return t.confirm(e.isLoaded, "cache emitted confirmed"), !0 182 | }) 183 | } 184 | }, a.prototype.confirm = function(e, t) { 185 | this.isConfirmed = !0, this.isLoaded = e, this.emit("confirm", this, t) 186 | }, a.prototype.handleEvent = function(e) { 187 | var t = "on" + e.type; 188 | this[t] && this[t](e) 189 | }, a.prototype.onload = function() { 190 | this.confirm(!0, "onload"), this.unbindProxyEvents() 191 | }, a.prototype.onerror = function() { 192 | this.confirm(!1, "onerror"), this.unbindProxyEvents() 193 | }, a.prototype.unbindProxyEvents = function() { 194 | n.unbind(this.proxyImage, "load", this), n.unbind(this.proxyImage, "error", this) 195 | }, r 196 | } 197 | var s = e.jQuery, 198 | o = e.console, 199 | h = o !== void 0, 200 | a = Object.prototype.toString; 201 | "function" == typeof define && define.amd ? define(["eventEmitter", "eventie"], r) : e.imagesLoaded = r(e.EventEmitter, e.eventie) 202 | }(window); -------------------------------------------------------------------------------- /pyportify/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from asyncio.locks import Semaphore 3 | import json 4 | import ssl 5 | 6 | import aiohttp 7 | import certifi 8 | import os 9 | import sys 10 | 11 | from aiohttp import web, ClientSession 12 | from aiohttp.web import json_response 13 | 14 | from pyportify.google import Mobileclient 15 | from pyportify.middlewares import IndexMiddleware 16 | from pyportify.serializers import Track 17 | from pyportify.spotify import SpotifyClient, SpotifyQuery 18 | from pyportify.util import uprint, find_closest_match, grouper 19 | 20 | IS_BUNDLED = getattr(sys, 'frozen', False) 21 | 22 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 23 | 24 | if IS_BUNDLED: 25 | STATIC_ROOT = os.path.dirname(sys.modules['pyportify'].__file__) 26 | else: 27 | STATIC_ROOT = os.path.join(BASE_DIR, "static") 28 | 29 | 30 | class UserScope(object): 31 | 32 | def __init__(self): 33 | self.google_token = None 34 | self.spotify_token = None 35 | 36 | 37 | user_scope = UserScope() 38 | semaphore = Semaphore(20) 39 | 40 | 41 | async def google_login(request): 42 | 43 | data = await request.json() 44 | 45 | email = data.get("email") 46 | password = data.get("password") 47 | 48 | with ClientSession() as session: 49 | g = Mobileclient(session) 50 | token = await g.login(email, password) 51 | if not token: 52 | return json_response(dict( 53 | status=400, 54 | message="login failed.", 55 | )) 56 | user_scope.google_token = token 57 | return json_response({"status": 200, 58 | "message": "login successful."}) 59 | 60 | 61 | async def spotify_login(request): 62 | 63 | data = await request.json() 64 | 65 | oauth_token = data.get("oauthToken") 66 | with ClientSession() as session: 67 | c = SpotifyClient(session, oauth_token) 68 | logged_in = await c.loggedin() 69 | if not logged_in: 70 | return json_response({"status": 400, 71 | "message": "login failed."}) 72 | user_scope.spotify_token = oauth_token 73 | 74 | return json_response({"status": 200, 75 | "message": "login successful."}) 76 | 77 | 78 | async def transfer_start(request): 79 | 80 | lists = await request.json() 81 | lists = [l['uri'] for l in lists] 82 | 83 | if not user_scope.google_token: 84 | return json_response({"status": 401, 85 | "message": "Google: not logged in."}) 86 | 87 | if not user_scope.spotify_token: 88 | return json_response({"status": 402, 89 | "message": "Spotify: not logged in."}) 90 | 91 | if not lists: 92 | return json_response({ 93 | "status": 403, 94 | "message": "Please select at least one playlist.", 95 | }) 96 | 97 | sslcontext = ssl.create_default_context(cafile=certifi.where()) 98 | conn = aiohttp.TCPConnector(ssl_context=sslcontext) 99 | 100 | with ClientSession(connector=conn) as session: 101 | g = Mobileclient(session, user_scope.google_token) 102 | s = SpotifyClient(session, user_scope.spotify_token) 103 | 104 | await transfer_playlists(request, s, g, lists) 105 | return json_response({"status": 200, 106 | "message": "transfer will start."}) 107 | 108 | 109 | async def spotify_playlists(request): 110 | with ClientSession() as session: 111 | c = SpotifyClient(session, user_scope.spotify_token) 112 | ret_playlists = await c.fetch_spotify_playlists() 113 | return json_response({"status": 200, 114 | "message": "ok", 115 | "data": ret_playlists}) 116 | 117 | 118 | async def transfer_playlists(request, s, g, sp_playlist_uris): 119 | for sp_playlist_uri in sp_playlist_uris: 120 | sp_playlist = await s.fetch_playlist(sp_playlist_uri) 121 | sp_playlist_tracks = await s.fetch_playlist_tracks( 122 | sp_playlist_uri) 123 | 124 | track_count = len(sp_playlist_tracks) 125 | if track_count == 0: 126 | uprint( 127 | "Skipping empty playlist %s" % 128 | (sp_playlist['name']) 129 | ) 130 | continue 131 | 132 | uprint( 133 | "Gathering tracks for playlist %s (%s)" % 134 | (sp_playlist['name'], track_count) 135 | ) 136 | playlist_json = { 137 | "playlist": {"uri": sp_playlist_uri, 138 | "name": sp_playlist['name']}, 139 | "name": sp_playlist['name'], 140 | } 141 | 142 | await emit_playlist_length(request, track_count) 143 | await emit_playlist_started(request, playlist_json) 144 | 145 | if not sp_playlist_tracks: 146 | await emit_playlist_ended(request, playlist_json) 147 | return 148 | 149 | tasks = [] 150 | for i, sp_track in enumerate(sp_playlist_tracks): 151 | query = SpotifyQuery(i, sp_playlist_uri, sp_track, track_count) 152 | future = search_gm_track(request, g, query) 153 | tasks.append(future) 154 | 155 | done = await asyncio.gather(*tasks) 156 | gm_track_ids = [i for i in done if i is not None] 157 | 158 | # Once we have all the gm_trackids, add them 159 | if len(gm_track_ids) > 0: 160 | uprint("Creating in Google Music... ", end='') 161 | sys.stdout.flush() 162 | for i, sub_gm_track_ids in enumerate(grouper(gm_track_ids, 1000)): 163 | name = sp_playlist['name'] 164 | if i > 0: 165 | name = "{} ({})".format(name, i+1) 166 | playlist_id = await g.create_playlist(name) 167 | await \ 168 | g.add_songs_to_playlist(playlist_id, sub_gm_track_ids) 169 | uprint("Done") 170 | 171 | await emit_playlist_ended(request, playlist_json) 172 | await emit_all_done(request) 173 | 174 | 175 | async def emit(request, event, data): 176 | if request is None: 177 | # uprint("Not emitting {0}".format(event)) 178 | return 179 | 180 | for ws in request.app['sockets']: 181 | ws.send_str(json.dumps({'eventName': event, 'eventData': data})) 182 | 183 | 184 | async def emit_added_event(request, found, sp_playlist_uri, search_query): 185 | await emit(request, "gmusic", { 186 | "type": "added" if found else "not_added", 187 | "data": {"spotify_track_uri": sp_playlist_uri, 188 | "spotify_track_name": search_query, 189 | "found": found, 190 | "karaoke": False}}) 191 | 192 | 193 | async def emit_playlist_length(request, track_count): 194 | await emit(request, "portify", 195 | {"type": "playlist_length", 196 | "data": {"length": track_count}}) 197 | 198 | 199 | async def emit_playlist_started(request, playlist_json): 200 | await emit(request, "portify", 201 | {"type": "playlist_started", "data": playlist_json}) 202 | 203 | 204 | async def emit_playlist_ended(request, playlist_json): 205 | await emit(request, "portify", 206 | {"type": "playlist_ended", "data": playlist_json}) 207 | 208 | 209 | async def emit_all_done(request): 210 | await emit(request, "portify", {"type": "all_done", "data": None}) 211 | 212 | 213 | async def search_gm_track(request, g, sp_query): 214 | with (await semaphore): 215 | track = None 216 | search_query = sp_query.search_query() 217 | if search_query: 218 | tracks = await g.find_best_tracks(search_query) 219 | serialized_tracks = [Track.from_gpm(track) for track in tracks] 220 | track = find_closest_match(sp_query.track, serialized_tracks) 221 | if track: 222 | gm_log_found(sp_query) 223 | await emit_added_event(request, True, 224 | sp_query.playlist_uri, search_query) 225 | return track.track_id 226 | 227 | gm_log_not_found(sp_query) 228 | await emit_added_event(request, False, 229 | sp_query.playlist_uri, search_query) 230 | return None 231 | 232 | 233 | async def wshandler(request): 234 | resp = web.WebSocketResponse() 235 | ws_ready = resp.can_prepare(request) 236 | if not ws_ready.ok: 237 | raise Exception("Couldn't start websocket") 238 | 239 | await resp.prepare(request) 240 | request.app['sockets'].append(resp) 241 | 242 | while True: 243 | msg = await resp.receive() 244 | 245 | if msg.tp == web.MsgType.text: 246 | pass 247 | else: 248 | break 249 | 250 | request.app['sockets'].remove(resp) 251 | return resp 252 | 253 | 254 | def gm_log_found(sp_query): 255 | uprint("({0}/{1}) Found '{2}' in Google Music".format( 256 | sp_query.i+1, sp_query.track_count, sp_query.search_query())) 257 | 258 | 259 | def gm_log_not_found(sp_query): 260 | uprint("({0}/{1}) No match found for '{2}' in Google Music".format( 261 | sp_query.i+1, sp_query.track_count, sp_query.search_query())) 262 | 263 | 264 | async def setup(loop): 265 | app1 = web.Application(loop=loop, middlewares=[IndexMiddleware()]) 266 | app1['sockets'] = [] 267 | app1.router.add_route('POST', '/google/login', google_login) 268 | app1.router.add_route('POST', '/spotify/login', spotify_login) 269 | app1.router.add_route('POST', '/portify/transfer/start', transfer_start) 270 | app1.router.add_route('GET', '/spotify/playlists', spotify_playlists) 271 | app1.router.add_route('GET', '/ws/', wshandler) 272 | app1.router.add_static('/', STATIC_ROOT) 273 | 274 | handler1 = app1.make_handler() 275 | 276 | await loop.create_server(handler1, '0.0.0.0', 3132) 277 | 278 | uprint("Listening on http://0.0.0.0:3132") 279 | uprint("Please open your browser window to http://localhost:3132") 280 | 281 | return handler1 282 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /pyportify/static/js/jquery-ui-1.10.3.custom.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.10.3 - 2013-05-31 2 | * http://jqueryui.com 3 | * Includes: jquery.ui.effect.js 4 | * Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ 5 | 6 | (function(t,e){var i="ui-effects-";t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=l(),n=s._rgba=[];return i=i.toLowerCase(),f(h,function(t,a){var o,r=a.re.exec(i),h=r&&a.parse(r),l=a.space||"rgba";return h?(o=s[l](h),s[c[l].cache]=o[c[l].cache],n=s._rgba=o._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,a.transparent),s):a[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var a,o="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,h=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],l=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=l.support={},p=t("

")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),l.fn=t.extend(l.prototype,{parse:function(n,o,r,h){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(o),o=e);var u=this,d=t.type(n),p=this._rgba=[];return o!==e&&(n=[n,o,r,h],d="array"),"string"===d?this.parse(s(n)||a._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof l?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var a=s.cache;f(s.props,function(t,e){if(!u[a]&&s.to){if("alpha"===t||null==n[t])return;u[a]=s.to(u._rgba)}u[a][e.idx]=i(n[t],e,!0)}),u[a]&&0>t.inArray(null,u[a].slice(0,3))&&(u[a][3]=1,s.from&&(u._rgba=s.from(u[a])))}),this):e},is:function(t){var i=l(t),s=!0,n=this;return f(c,function(t,a){var o,r=i[a.cache];return r&&(o=n[a.cache]||a.to&&a.to(n._rgba)||[],f(a.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===o[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=l(t),n=s._space(),a=c[n],o=0===this.alpha()?l("transparent"):this,r=o[a.cache]||a.to(o._rgba),h=r.slice();return s=s[a.cache],f(a.props,function(t,n){var a=n.idx,o=r[a],l=s[a],c=u[n.type]||{};null!==l&&(null===o?h[a]=l:(c.mod&&(l-o>c.mod/2?o+=c.mod:o-l>c.mod/2&&(o-=c.mod)),h[a]=i((l-o)*e+o,n)))}),this[n](h)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=l(e)._rgba;return l(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),l.fn.parse.prototype=l.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,a=t[2]/255,o=t[3],r=Math.max(s,n,a),h=Math.min(s,n,a),l=r-h,c=r+h,u=.5*c;return e=h===r?0:s===r?60*(n-a)/l+360:n===r?60*(a-s)/l+120:60*(s-n)/l+240,i=0===l?0:.5>=u?l/c:l/(2-c),[Math.round(e)%360,i,u,null==o?1:o]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],a=t[3],o=.5>=s?s*(1+i):s+i-s*i,r=2*s-o;return[Math.round(255*n(r,o,e+1/3)),Math.round(255*n(r,o,e)),Math.round(255*n(r,o,e-1/3)),a]},f(c,function(s,n){var a=n.props,o=n.cache,h=n.to,c=n.from;l.fn[s]=function(s){if(h&&!this[o]&&(this[o]=h(this._rgba)),s===e)return this[o].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[o].slice();return f(a,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=l(c(d)),n[o]=d,n):l(d)},f(a,function(e,i){l.fn[e]||(l.fn[e]=function(n){var a,o=t.type(n),h="alpha"===e?this._hsla?"hsla":"rgba":s,l=this[h](),c=l[i.idx];return"undefined"===o?c:("function"===o&&(n=n.call(this,c),o=t.type(n)),null==n&&i.empty?this:("string"===o&&(a=r.exec(n),a&&(n=c+parseFloat(a[2])*("+"===a[1]?1:-1))),l[i.idx]=n,this[h](l)))})})}),l.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var a,o,r="";if("transparent"!==n&&("string"!==t.type(n)||(a=s(n)))){if(n=l(a||n),!d.rgba&&1!==n._rgba[3]){for(o="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&o&&o.style;)try{r=t.css(o,"backgroundColor"),o=o.parentNode}catch(h){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(h){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=l(e.elem,i),e.end=l(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},l.hook(o),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},a=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(jQuery),function(){function i(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,a={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(a[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(a[i]=n[i]);return a}function s(e,i){var s,n,o={};for(s in i)n=i[s],e[s]!==n&&(a[s]||(t.fx.step[s]||!isNaN(parseFloat(n)))&&(o[s]=n));return o}var n=["add","remove","toggle"],a={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(jQuery.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(e,a,o,r){var h=t.speed(a,o,r);return this.queue(function(){var a,o=t(this),r=o.attr("class")||"",l=h.children?o.find("*").addBack():o;l=l.map(function(){var e=t(this);return{el:e,start:i(this)}}),a=function(){t.each(n,function(t,i){e[i]&&o[i+"Class"](e[i])})},a(),l=l.map(function(){return this.end=i(this.el[0]),this.diff=s(this.start,this.end),this}),o.attr("class",r),l=l.map(function(){var e=this,i=t.Deferred(),s=t.extend({},h,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,l.get()).done(function(){a(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),h.complete.call(o[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,a){return s?t.effects.animateClass.call(this,{add:i},s,n,a):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,a){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,a):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(i){return function(s,n,a,o,r){return"boolean"==typeof n||n===e?a?t.effects.animateClass.call(this,n?{add:s}:{remove:s},a,o,r):i.apply(this,arguments):t.effects.animateClass.call(this,{toggle:s},n,a,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,a){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,a)}})}(),function(){function s(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function n(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}t.extend(t.effects,{version:"1.10.3",save:function(t,e){for(var s=0;e.length>s;s++)null!==e[s]&&t.data(i+e[s],t[0].style[e[s]])},restore:function(t,s){var n,a;for(a=0;s.length>a;a++)null!==s[a]&&(n=t.data(i+s[a]),n===e&&(n=""),t.css(s[a],n))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("

").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},a=document.activeElement;try{a.id}catch(o){a=document.body}return e.wrap(s),(e[0]===a||t.contains(e[0],a))&&t(a).focus(),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).focus()),e},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var a=e.cssUnit(i);a[0]>0&&(n[i]=a[0]*s+a[1])}),n}}),t.fn.extend({effect:function(){function e(e){function s(){t.isFunction(a)&&a.call(n[0]),t.isFunction(e)&&e()}var n=t(this),a=i.complete,r=i.mode;(n.is(":hidden")?"hide"===r:"show"===r)?(n[r](),s()):o.call(n[0],i,s)}var i=s.apply(this,arguments),n=i.mode,a=i.queue,o=t.effects.effect[i.effect];return t.fx.off||!o?n?this[n](i.duration,i.complete):this.each(function(){i.complete&&i.complete.call(this)}):a===!1?this.each(e):this.queue(a||"fx",e)},show:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="show",this.effect.call(this,i)}}(t.fn.show),hide:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="hide",this.effect.call(this,i)}}(t.fn.hide),toggle:function(t){return function(e){if(n(e)||"boolean"==typeof e)return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s}})}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}()})(jQuery); -------------------------------------------------------------------------------- /pyportify/static/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /pyportify/static/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 3.1.0 3 | * the iconic font designed for Bootstrap 4 | * ------------------------------------------------------- 5 | * The full suite of pictographic icons, examples, and documentation 6 | * can be found at: http://fontawesome.io 7 | * 8 | * License 9 | * ------------------------------------------------------- 10 | * - The Font Awesome font is licensed under the SIL Open Font License v1.1 - 11 | * http://scripts.sil.org/OFL 12 | * - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - 13 | * http://opensource.org/licenses/mit-license.html 14 | * - Font Awesome documentation licensed under CC BY 3.0 License - 15 | * http://creativecommons.org/licenses/by/3.0/ 16 | * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: 17 | * "Font Awesome by Dave Gandy - http://fontawesome.io" 18 | 19 | * Contact 20 | * ------------------------------------------------------- 21 | * Email: dave@fontawesome.io 22 | * Twitter: http://twitter.com/fortaweso_me 23 | * Work: Lead Product Designer @ http://kyruus.com 24 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=3.1.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=3.1.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=3.1.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=3.1.0') format('truetype'),url('../fonts/fontawesome-webfont.svg#fontawesomeregular?v=3.1.0') format('svg');font-weight:normal;font-style:normal}[class^="icon-"],[class*=" icon-"]{font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;*margin-right:.3em}[class^="icon-"]:before,[class*=" icon-"]:before{text-decoration:inherit;display:inline-block;speak:none}.icon-large:before{vertical-align:-10%;font-size:1.3333333333333333em}a [class^="icon-"],a [class*=" icon-"],a [class^="icon-"]:before,a [class*=" icon-"]:before{display:inline}[class^="icon-"].icon-fixed-width,[class*=" icon-"].icon-fixed-width{display:inline-block;width:1.2857142857142858em;text-align:center}[class^="icon-"].icon-fixed-width.icon-large,[class*=" icon-"].icon-fixed-width.icon-large{width:1.5714285714285714em}ul.icons-ul{list-style-type:none;text-indent:-0.7142857142857143em;margin-left:2.142857142857143em}ul.icons-ul>li .icon-li{width:.7142857142857143em;display:inline-block;text-align:center}[class^="icon-"].hide,[class*=" icon-"].hide{display:none}.icon-muted{color:#eee}.icon-light{color:#fff}.icon-dark{color:#333}.icon-border{border:solid 1px #eee;padding:.2em .25em .15em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.icon-2x{font-size:2em}.icon-2x.icon-border{border-width:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.icon-3x{font-size:3em}.icon-3x.icon-border{border-width:3px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.icon-4x{font-size:4em}.icon-4x.icon-border{border-width:4px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.icon-5x{font-size:5em}.icon-5x.icon-border{border-width:5px;-webkit-border-radius:7px;-moz-border-radius:7px;border-radius:7px}.pull-right{float:right}.pull-left{float:left}[class^="icon-"].pull-left,[class*=" icon-"].pull-left{margin-right:.3em}[class^="icon-"].pull-right,[class*=" icon-"].pull-right{margin-left:.3em}[class^="icon-"],[class*=" icon-"]{display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none;background-position:0 0;background-repeat:repeat;margin-top:0}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"]{background-image:none}.btn [class^="icon-"].icon-large,.nav [class^="icon-"].icon-large,.btn [class*=" icon-"].icon-large,.nav [class*=" icon-"].icon-large{line-height:.9em}.btn [class^="icon-"].icon-spin,.nav [class^="icon-"].icon-spin,.btn [class*=" icon-"].icon-spin,.nav [class*=" icon-"].icon-spin{display:inline-block}.nav-tabs [class^="icon-"],.nav-pills [class^="icon-"],.nav-tabs [class*=" icon-"],.nav-pills [class*=" icon-"],.nav-tabs [class^="icon-"].icon-large,.nav-pills [class^="icon-"].icon-large,.nav-tabs [class*=" icon-"].icon-large,.nav-pills [class*=" icon-"].icon-large{line-height:.9em}.btn [class^="icon-"].pull-left.icon-2x,.btn [class*=" icon-"].pull-left.icon-2x,.btn [class^="icon-"].pull-right.icon-2x,.btn [class*=" icon-"].pull-right.icon-2x{margin-top:.18em}.btn [class^="icon-"].icon-spin.icon-large,.btn [class*=" icon-"].icon-spin.icon-large{line-height:.8em}.btn.btn-small [class^="icon-"].pull-left.icon-2x,.btn.btn-small [class*=" icon-"].pull-left.icon-2x,.btn.btn-small [class^="icon-"].pull-right.icon-2x,.btn.btn-small [class*=" icon-"].pull-right.icon-2x{margin-top:.25em}.btn.btn-large [class^="icon-"],.btn.btn-large [class*=" icon-"]{margin-top:0}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-top:.05em}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x{margin-right:.2em}.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-left:.2em}.icon-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:-35%}.icon-stack [class^="icon-"],.icon-stack [class*=" icon-"]{display:block;text-align:center;position:absolute;width:100%;height:100%;font-size:1em;line-height:inherit;*line-height:2em}.icon-stack .icon-stack-base{font-size:2em;*line-height:1em}.icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.icon-rotate-90:before{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1)}.icon-rotate-180:before{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2)}.icon-rotate-270:before{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3)}.icon-flip-horizontal:before{-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.icon-flip-vertical:before{-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.icon-glass:before{content:"\f000"}.icon-music:before{content:"\f001"}.icon-search:before{content:"\f002"}.icon-envelope:before{content:"\f003"}.icon-heart:before{content:"\f004"}.icon-star:before{content:"\f005"}.icon-star-empty:before{content:"\f006"}.icon-user:before{content:"\f007"}.icon-film:before{content:"\f008"}.icon-th-large:before{content:"\f009"}.icon-th:before{content:"\f00a"}.icon-th-list:before{content:"\f00b"}.icon-ok:before{content:"\f00c"}.icon-remove:before{content:"\f00d"}.icon-zoom-in:before{content:"\f00e"}.icon-zoom-out:before{content:"\f010"}.icon-off:before{content:"\f011"}.icon-signal:before{content:"\f012"}.icon-cog:before{content:"\f013"}.icon-trash:before{content:"\f014"}.icon-home:before{content:"\f015"}.icon-file:before{content:"\f016"}.icon-time:before{content:"\f017"}.icon-road:before{content:"\f018"}.icon-download-alt:before{content:"\f019"}.icon-download:before{content:"\f01a"}.icon-upload:before{content:"\f01b"}.icon-inbox:before{content:"\f01c"}.icon-play-circle:before{content:"\f01d"}.icon-repeat:before,.icon-rotate-right:before{content:"\f01e"}.icon-refresh:before{content:"\f021"}.icon-list-alt:before{content:"\f022"}.icon-lock:before{content:"\f023"}.icon-flag:before{content:"\f024"}.icon-headphones:before{content:"\f025"}.icon-volume-off:before{content:"\f026"}.icon-volume-down:before{content:"\f027"}.icon-volume-up:before{content:"\f028"}.icon-qrcode:before{content:"\f029"}.icon-barcode:before{content:"\f02a"}.icon-tag:before{content:"\f02b"}.icon-tags:before{content:"\f02c"}.icon-book:before{content:"\f02d"}.icon-bookmark:before{content:"\f02e"}.icon-print:before{content:"\f02f"}.icon-camera:before{content:"\f030"}.icon-font:before{content:"\f031"}.icon-bold:before{content:"\f032"}.icon-italic:before{content:"\f033"}.icon-text-height:before{content:"\f034"}.icon-text-width:before{content:"\f035"}.icon-align-left:before{content:"\f036"}.icon-align-center:before{content:"\f037"}.icon-align-right:before{content:"\f038"}.icon-align-justify:before{content:"\f039"}.icon-list:before{content:"\f03a"}.icon-indent-left:before{content:"\f03b"}.icon-indent-right:before{content:"\f03c"}.icon-facetime-video:before{content:"\f03d"}.icon-picture:before{content:"\f03e"}.icon-pencil:before{content:"\f040"}.icon-map-marker:before{content:"\f041"}.icon-adjust:before{content:"\f042"}.icon-tint:before{content:"\f043"}.icon-edit:before{content:"\f044"}.icon-share:before{content:"\f045"}.icon-check:before{content:"\f046"}.icon-move:before{content:"\f047"}.icon-step-backward:before{content:"\f048"}.icon-fast-backward:before{content:"\f049"}.icon-backward:before{content:"\f04a"}.icon-play:before{content:"\f04b"}.icon-pause:before{content:"\f04c"}.icon-stop:before{content:"\f04d"}.icon-forward:before{content:"\f04e"}.icon-fast-forward:before{content:"\f050"}.icon-step-forward:before{content:"\f051"}.icon-eject:before{content:"\f052"}.icon-chevron-left:before{content:"\f053"}.icon-chevron-right:before{content:"\f054"}.icon-plus-sign:before{content:"\f055"}.icon-minus-sign:before{content:"\f056"}.icon-remove-sign:before{content:"\f057"}.icon-ok-sign:before{content:"\f058"}.icon-question-sign:before{content:"\f059"}.icon-info-sign:before{content:"\f05a"}.icon-screenshot:before{content:"\f05b"}.icon-remove-circle:before{content:"\f05c"}.icon-ok-circle:before{content:"\f05d"}.icon-ban-circle:before{content:"\f05e"}.icon-arrow-left:before{content:"\f060"}.icon-arrow-right:before{content:"\f061"}.icon-arrow-up:before{content:"\f062"}.icon-arrow-down:before{content:"\f063"}.icon-share-alt:before,.icon-mail-forward:before{content:"\f064"}.icon-resize-full:before{content:"\f065"}.icon-resize-small:before{content:"\f066"}.icon-plus:before{content:"\f067"}.icon-minus:before{content:"\f068"}.icon-asterisk:before{content:"\f069"}.icon-exclamation-sign:before{content:"\f06a"}.icon-gift:before{content:"\f06b"}.icon-leaf:before{content:"\f06c"}.icon-fire:before{content:"\f06d"}.icon-eye-open:before{content:"\f06e"}.icon-eye-close:before{content:"\f070"}.icon-warning-sign:before{content:"\f071"}.icon-plane:before{content:"\f072"}.icon-calendar:before{content:"\f073"}.icon-random:before{content:"\f074"}.icon-comment:before{content:"\f075"}.icon-magnet:before{content:"\f076"}.icon-chevron-up:before{content:"\f077"}.icon-chevron-down:before{content:"\f078"}.icon-retweet:before{content:"\f079"}.icon-shopping-cart:before{content:"\f07a"}.icon-folder-close:before{content:"\f07b"}.icon-folder-open:before{content:"\f07c"}.icon-resize-vertical:before{content:"\f07d"}.icon-resize-horizontal:before{content:"\f07e"}.icon-bar-chart:before{content:"\f080"}.icon-twitter-sign:before{content:"\f081"}.icon-facebook-sign:before{content:"\f082"}.icon-camera-retro:before{content:"\f083"}.icon-key:before{content:"\f084"}.icon-cogs:before{content:"\f085"}.icon-comments:before{content:"\f086"}.icon-thumbs-up:before{content:"\f087"}.icon-thumbs-down:before{content:"\f088"}.icon-star-half:before{content:"\f089"}.icon-heart-empty:before{content:"\f08a"}.icon-signout:before{content:"\f08b"}.icon-linkedin-sign:before{content:"\f08c"}.icon-pushpin:before{content:"\f08d"}.icon-external-link:before{content:"\f08e"}.icon-signin:before{content:"\f090"}.icon-trophy:before{content:"\f091"}.icon-github-sign:before{content:"\f092"}.icon-upload-alt:before{content:"\f093"}.icon-lemon:before{content:"\f094"}.icon-phone:before{content:"\f095"}.icon-check-empty:before{content:"\f096"}.icon-bookmark-empty:before{content:"\f097"}.icon-phone-sign:before{content:"\f098"}.icon-twitter:before{content:"\f099"}.icon-facebook:before{content:"\f09a"}.icon-github:before{content:"\f09b"}.icon-unlock:before{content:"\f09c"}.icon-credit-card:before{content:"\f09d"}.icon-rss:before{content:"\f09e"}.icon-hdd:before{content:"\f0a0"}.icon-bullhorn:before{content:"\f0a1"}.icon-bell:before{content:"\f0a2"}.icon-certificate:before{content:"\f0a3"}.icon-hand-right:before{content:"\f0a4"}.icon-hand-left:before{content:"\f0a5"}.icon-hand-up:before{content:"\f0a6"}.icon-hand-down:before{content:"\f0a7"}.icon-circle-arrow-left:before{content:"\f0a8"}.icon-circle-arrow-right:before{content:"\f0a9"}.icon-circle-arrow-up:before{content:"\f0aa"}.icon-circle-arrow-down:before{content:"\f0ab"}.icon-globe:before{content:"\f0ac"}.icon-wrench:before{content:"\f0ad"}.icon-tasks:before{content:"\f0ae"}.icon-filter:before{content:"\f0b0"}.icon-briefcase:before{content:"\f0b1"}.icon-fullscreen:before{content:"\f0b2"}.icon-group:before{content:"\f0c0"}.icon-link:before{content:"\f0c1"}.icon-cloud:before{content:"\f0c2"}.icon-beaker:before{content:"\f0c3"}.icon-cut:before{content:"\f0c4"}.icon-copy:before{content:"\f0c5"}.icon-paper-clip:before{content:"\f0c6"}.icon-save:before{content:"\f0c7"}.icon-sign-blank:before{content:"\f0c8"}.icon-reorder:before{content:"\f0c9"}.icon-list-ul:before{content:"\f0ca"}.icon-list-ol:before{content:"\f0cb"}.icon-strikethrough:before{content:"\f0cc"}.icon-underline:before{content:"\f0cd"}.icon-table:before{content:"\f0ce"}.icon-magic:before{content:"\f0d0"}.icon-truck:before{content:"\f0d1"}.icon-pinterest:before{content:"\f0d2"}.icon-pinterest-sign:before{content:"\f0d3"}.icon-google-plus-sign:before{content:"\f0d4"}.icon-google-plus:before{content:"\f0d5"}.icon-money:before{content:"\f0d6"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-columns:before{content:"\f0db"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-envelope-alt:before{content:"\f0e0"}.icon-linkedin:before{content:"\f0e1"}.icon-undo:before,.icon-rotate-left:before{content:"\f0e2"}.icon-legal:before{content:"\f0e3"}.icon-dashboard:before{content:"\f0e4"}.icon-comment-alt:before{content:"\f0e5"}.icon-comments-alt:before{content:"\f0e6"}.icon-bolt:before{content:"\f0e7"}.icon-sitemap:before{content:"\f0e8"}.icon-umbrella:before{content:"\f0e9"}.icon-paste:before{content:"\f0ea"}.icon-lightbulb:before{content:"\f0eb"}.icon-exchange:before{content:"\f0ec"}.icon-cloud-download:before{content:"\f0ed"}.icon-cloud-upload:before{content:"\f0ee"}.icon-user-md:before{content:"\f0f0"}.icon-stethoscope:before{content:"\f0f1"}.icon-suitcase:before{content:"\f0f2"}.icon-bell-alt:before{content:"\f0f3"}.icon-coffee:before{content:"\f0f4"}.icon-food:before{content:"\f0f5"}.icon-file-alt:before{content:"\f0f6"}.icon-building:before{content:"\f0f7"}.icon-hospital:before{content:"\f0f8"}.icon-ambulance:before{content:"\f0f9"}.icon-medkit:before{content:"\f0fa"}.icon-fighter-jet:before{content:"\f0fb"}.icon-beer:before{content:"\f0fc"}.icon-h-sign:before{content:"\f0fd"}.icon-plus-sign-alt:before{content:"\f0fe"}.icon-double-angle-left:before{content:"\f100"}.icon-double-angle-right:before{content:"\f101"}.icon-double-angle-up:before{content:"\f102"}.icon-double-angle-down:before{content:"\f103"}.icon-angle-left:before{content:"\f104"}.icon-angle-right:before{content:"\f105"}.icon-angle-up:before{content:"\f106"}.icon-angle-down:before{content:"\f107"}.icon-desktop:before{content:"\f108"}.icon-laptop:before{content:"\f109"}.icon-tablet:before{content:"\f10a"}.icon-mobile-phone:before{content:"\f10b"}.icon-circle-blank:before{content:"\f10c"}.icon-quote-left:before{content:"\f10d"}.icon-quote-right:before{content:"\f10e"}.icon-spinner:before{content:"\f110"}.icon-circle:before{content:"\f111"}.icon-reply:before,.icon-mail-reply:before{content:"\f112"}.icon-folder-close-alt:before{content:"\f114"}.icon-folder-open-alt:before{content:"\f115"}.icon-expand-alt:before{content:"\f116"}.icon-collapse-alt:before{content:"\f117"}.icon-smile:before{content:"\f118"}.icon-frown:before{content:"\f119"}.icon-meh:before{content:"\f11a"}.icon-gamepad:before{content:"\f11b"}.icon-keyboard:before{content:"\f11c"}.icon-flag-alt:before{content:"\f11d"}.icon-flag-checkered:before{content:"\f11e"}.icon-terminal:before{content:"\f120"}.icon-code:before{content:"\f121"}.icon-reply-all:before{content:"\f122"}.icon-mail-reply-all:before{content:"\f122"}.icon-star-half-full:before,.icon-star-half-empty:before{content:"\f123"}.icon-location-arrow:before{content:"\f124"}.icon-crop:before{content:"\f125"}.icon-code-fork:before{content:"\f126"}.icon-unlink:before{content:"\f127"}.icon-question:before{content:"\f128"}.icon-info:before{content:"\f129"}.icon-exclamation:before{content:"\f12a"}.icon-superscript:before{content:"\f12b"}.icon-subscript:before{content:"\f12c"}.icon-eraser:before{content:"\f12d"}.icon-puzzle-piece:before{content:"\f12e"}.icon-microphone:before{content:"\f130"}.icon-microphone-off:before{content:"\f131"}.icon-shield:before{content:"\f132"}.icon-calendar-empty:before{content:"\f133"}.icon-fire-extinguisher:before{content:"\f134"}.icon-rocket:before{content:"\f135"}.icon-maxcdn:before{content:"\f136"}.icon-chevron-sign-left:before{content:"\f137"}.icon-chevron-sign-right:before{content:"\f138"}.icon-chevron-sign-up:before{content:"\f139"}.icon-chevron-sign-down:before{content:"\f13a"}.icon-html5:before{content:"\f13b"}.icon-css3:before{content:"\f13c"}.icon-anchor:before{content:"\f13d"}.icon-unlock-alt:before{content:"\f13e"}.icon-bullseye:before{content:"\f140"}.icon-ellipsis-horizontal:before{content:"\f141"}.icon-ellipsis-vertical:before{content:"\f142"}.icon-rss-sign:before{content:"\f143"}.icon-play-sign:before{content:"\f144"}.icon-ticket:before{content:"\f145"}.icon-minus-sign-alt:before{content:"\f146"}.icon-check-minus:before{content:"\f147"}.icon-level-up:before{content:"\f148"}.icon-level-down:before{content:"\f149"}.icon-check-sign:before{content:"\f14a"}.icon-edit-sign:before{content:"\f14b"}.icon-external-link-sign:before{content:"\f14c"}.icon-share-sign:before{content:"\f14d"} -------------------------------------------------------------------------------- /pyportify/static/fonts/Flat-UI-Icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG font generated by IcoMoon. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 28 | 30 | 34 | 37 | 41 | 46 | 51 | 56 | 58 | 62 | 68 | 72 | 76 | 79 | 83 | 90 | 93 | 95 | 112 | 114 | 117 | 118 | 123 | 127 | 130 | 133 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /pyportify/static/fonts/Flat-UI-Icons.dev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG font generated by IcoMoon. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 28 | 30 | 34 | 37 | 41 | 46 | 51 | 56 | 58 | 62 | 68 | 72 | 76 | 79 | 83 | 90 | 93 | 95 | 112 | 114 | 117 | 118 | 123 | 127 | 130 | 133 | 138 | 139 | 140 | --------------------------------------------------------------------------------