This commit is contained in:
R. M
2022-11-15 22:16:18 -03:00
parent 397e76ee2d
commit c805e5bd60
26 changed files with 6034 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
/*
__pycache__/
!gamdl.py
!song_genres.py
!music_video_genres.py
!storefront_ids.py
!pywidevine
!requirements.txt
!.gitignore
device_client_id_blob
device_private_key
+504
View File
@@ -0,0 +1,504 @@
from pathlib import Path
import re
import requests
import urllib3
import storefront_ids
import json
import m3u8
from yt_dlp import YoutubeDL
from pywidevine.L3.decrypt.wvdecrypt import WvDecrypt
from pywidevine.L3.decrypt.wvdecryptconfig import WvDecryptConfig
import base64
from pywidevine.L3.cdm.formats.widevine_pssh_data_pb2 import WidevinePsshData
from mutagen.mp4 import MP4Cover, MP4
import song_genres
import music_video_genres
from xml.dom import minidom
import datetime
import os
from argparse import ArgumentParser
import shutil
import traceback
class Gamdl:
def __init__(self, disable_music_video_skip, auth_path, temp_path, prefer_hevc, final_path):
self.disable_music_video_skip = disable_music_video_skip
self.auth_path = auth_path
self.temp_path = temp_path
self.prefer_hevc = prefer_hevc
self.final_path = final_path
self.login()
def login(self):
cookies = {}
with open(Path(self.auth_path) / 'cookies.txt', 'r') as f:
for l in f:
if not re.match(r"^#", l) and not re.match(r"^\n", l):
line_fields = l.strip().replace('"', '"').split('\t')
cookies[line_fields[5]] = line_fields[6]
with open(Path(self.auth_path) / 'token.txt', 'r') as f:
token = f.read()
self.session = requests.Session()
self.session.verify = False
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
self.session.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0"})
self.session.cookies.update(cookies)
self.session.headers.update({
"Accept": "application/json",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"authorization": token,
"content-type": "application/json",
"Media-User-Token": self.session.cookies.get_dict()["media-user-token"],
"x-apple-renewal": "true",
"DNT": "1",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
'origin': 'https://beta.music.apple.com'
})
self.country = cookies['itua']
self.storefront = getattr(storefront_ids, self.country)
def get_download_queue(self, url):
download_queue = []
product_id = url.split('/')[-1].split('i=')[-1].split('&')[0].split('?')[0]
response = self.session.get(f'https://api.music.apple.com/v1/catalog/{self.country}/?ids[songs]={product_id}&ids[albums]={product_id}&ids[playlists]={product_id}&ids[music-videos]={product_id}').json()['data'][0]
if response['type'] == 'songs':
download_queue.append({
'track_id': response['id'],
'title': response['attributes']['name'],
})
if response['type'] == 'albums' or response['type'] == 'playlists':
for track in response['relationships']['tracks']['data']:
if 'playParams' in track['attributes'].keys():
if track['type'] == 'music-videos' and self.disable_music_video_skip:
download_queue.append({
'track_id': track['attributes']['playParams']['id'],
'alt_track_id': track['attributes']['url'].split('/')[-1],
'title': track['attributes']['name'],
})
if track['type'] == 'songs':
download_queue.append({
'track_id': track['attributes']['playParams']['id'],
'title': track['attributes']['name'],
})
if response['type'] == 'music-videos':
download_queue.append({
'track_id': response['attributes']['playParams']['id'],
'alt_track_id': response['attributes']['url'].split('/')[-1],
'title': response['attributes']['name'],
})
return download_queue
def get_webplayback(self, track_id):
response = self.session.post(
'https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/webPlayback',
json.dumps({
'salableAdamId': track_id
})
).json()["songList"][0]
return response
def get_playlist_music_video(self, webplayback):
return m3u8.load(webplayback['hls-playlist-url'])
def get_stream_url_song(self, webplayback):
return next((x for x in webplayback["assets"] if x["flavor"] == "28:ctrp256"))['URL']
def get_stream_url_music_video_audio(self, playlist):
return [x for x in playlist.media if x.type == "AUDIO"][-1].uri
def get_stream_url_music_video_video(self, playlist):
if self.prefer_hevc:
return playlist.playlists[-1].uri
else:
return [x for x in playlist.playlists if 'avc' in x.stream_info.codecs][-1].uri
def get_encrypted_location_audio(self, track_id):
return Path(self.temp_path) / f'{track_id}e.m4a'
def get_encrypted_location_video(self, track_id):
return Path(self.temp_path) / f'{track_id}e.mp4'
def get_decrypted_location_audio(self, track_id):
return Path(self.temp_path) / f'{track_id}d.m4a'
def get_decrypted_location_video(self, track_id):
return Path(self.temp_path) / f'{track_id}d.mp4'
def download(self, encrypted_location, stream_url):
with YoutubeDL({
'quiet': True,
'no_warnings': True,
'outtmpl': str(encrypted_location),
'allow_unplayable_formats': True,
'fixup': 'never',
'external_downloader': 'aria2c'
}) as ydl:
ydl.download(stream_url)
def get_license_b64(self, challenge, track_uri, track_id):
return self.session.post(
'https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense',
json.dumps({
'challenge': challenge,
'key-system': 'com.widevine.alpha',
'uri': track_uri,
'adamId': track_id,
'isLibrary': False,
'user-initiated': True
})
).json()['license']
def decrypt_music_video(self, decrypted_location, encrypted_location, stream_url, track_id):
playlist = m3u8.load(stream_url)
track_uri = next(x for x in playlist.keys if x.keyformat == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed").uri
wvdecryptconfig = WvDecryptConfig(decrypted_location, encrypted_location, track_uri)
wvdecryptconfig.init_data_b64 = wvdecryptconfig.init_data_b64.split(",")[1]
wvdecrypt = WvDecrypt(wvdecryptconfig)
challenge = base64.b64encode(wvdecrypt.get_challenge()).decode('utf-8')
license_b64 = self.get_license_b64(challenge, track_uri, track_id)
wvdecrypt.update_license(license_b64)
wvdecrypt.start_process()
def decrypt_song(self, decrypted_location, encrypted_location, stream_url, track_id):
track_uri = m3u8.load(stream_url).keys[0].uri
wvpsshdata = WidevinePsshData()
wvpsshdata.algorithm = 1
wvdecryptconfig = WvDecryptConfig(decrypted_location, encrypted_location, track_uri)
wvpsshdata.key_id.append(base64.b64decode(wvdecryptconfig.init_data_b64.split(",")[1]))
wvdecryptconfig.init_data_b64 = base64.b64encode(wvpsshdata.SerializeToString()).decode("utf8")
wvdecrypt = WvDecrypt(wvdecryptconfig)
challenge = base64.b64encode(wvdecrypt.get_challenge()).decode('utf-8')
license_b64 = self.get_license_b64(challenge, track_uri, track_id)
wvdecrypt.update_license(license_b64)
wvdecrypt.start_process()
def get_synced_lyrics_formated_time(self, unformatted_time):
if 's' in unformatted_time:
unformatted_time = unformatted_time.replace('s', '')
if '.' not in unformatted_time:
unformatted_time += '.0'
s = int(unformatted_time.split('.')[-2].split(':')[-1]) * 1000
try:
m = int(unformatted_time.split('.')[-2].split(':')[-2]) * 60000
except:
m = 0
ms = int(unformatted_time.split('.')[-1])
formated_time = datetime.datetime.fromtimestamp((s + m + ms)/1000.0)
return f'{formated_time.minute:02d}:{formated_time.second:02d}.{int(str(formated_time.microsecond)[:2]):02d}'
def get_lyrics(self, track_id):
try:
raw_lyrics = minidom.parseString(self.session.get(f'https://amp-api.music.apple.com/v1/catalog/{self.country}/songs/{track_id}/lyrics').json()['data'][0]['attributes']['ttml'])
except:
return
unsynced_lyrics = ''
synced_lyrics = ''
for stanza in raw_lyrics.getElementsByTagName("div"):
for verse in stanza.getElementsByTagName("p"):
if not verse.firstChild.nodeValue:
subverse_time = []
subverse_text = []
for subserve in verse.getElementsByTagName("span"):
if subserve.firstChild.nodeValue:
subverse_time.append(subserve.getAttribute('begin'))
subverse_text.append(subserve.firstChild.nodeValue)
subverse_time = subverse_time[0]
subverse_text = ' '.join(subverse_text)
unsynced_lyrics += subverse_text + '\n'
if subverse_time:
synced_lyrics += f'[{self.get_synced_lyrics_formated_time(subverse_time)}]{subverse_text}\n'
else:
unsynced_lyrics += verse.firstChild.nodeValue + '\n'
if verse.getAttribute('begin'):
synced_lyrics += f'[{self.get_synced_lyrics_formated_time(verse.getAttribute("begin"))}]{verse.firstChild.nodeValue}\n'
unsynced_lyrics += '\n'
return [unsynced_lyrics.strip(), synced_lyrics]
def get_tags_song(self, webplayback, lyrics):
metadata = next((x for x in webplayback["assets"] if x["flavor"] == "28:ctrp256"))['metadata']
artwork_url = next((x for x in webplayback["assets"] if x["flavor"] == "28:ctrp256"))['artworkURL']
tags = {
'\xa9nam': [metadata['itemName']],
'\xa9gen': [getattr(song_genres, f'ID{metadata["genreId"]}')],
'aART': [metadata['playlistArtistName']],
'\xa9alb': [metadata['playlistName']],
'soar': [metadata['sort-artist']],
'soal': [metadata['sort-album']],
'sonm': [metadata['sort-name']],
'\xa9alb': [metadata['playlistName']],
'\xa9ART': [metadata['artistName']],
'geID': [metadata['genreId']],
'atID': [int(metadata['artistId'])],
'plID': [int(metadata['playlistId'])],
'cnID': [int(metadata['itemId'])],
'sfID': [metadata['s']],
'rtng': [metadata['explicit']],
'pgap': metadata['gapless'],
'cpil': metadata['compilation'],
'disk': [(metadata['discNumber'], metadata['discCount'])],
'trkn': [(metadata['trackNumber'], metadata['trackCount'])],
'covr': [MP4Cover(requests.get(artwork_url).content, MP4Cover.FORMAT_JPEG)],
'stik': [1]
}
if 'copyright' in metadata.keys():
tags['cprt'] = [metadata['copyright']]
if 'releaseDate' in metadata.keys():
tags['\xa9day'] = [metadata['releaseDate']]
if 'comments' in metadata.keys():
tags['\xa9cmt'] = [metadata['comments']]
if 'xid' in metadata.keys():
tags['xid '] = [metadata['xid']]
if 'composerId' in metadata.keys():
tags['cmID'] = [int(metadata['composerId'])]
tags['\xa9wrt'] = [metadata['composerName']]
tags['soco'] = [metadata['sort-composer']]
if lyrics:
tags['\xa9lyr'] = [lyrics[0]]
return tags
def get_tags_music_video(self, track_id):
metadata = requests.get(f'https://itunes.apple.com/lookup?id={track_id}&entity=album&limit=200&country={self.country}').json()['results']
extra_metadata = requests.get(f'https://music.apple.com/music-video/{metadata[0]["trackId"]}', headers = {'X-Apple-Store-Front': f'{self.storefront} t:music31'}).json()['storePlatformData']['product-dv']['results'][str(metadata[0]['trackId'])]
tags = {
'\xa9ART': [metadata[0]["artistName"]],
'\xa9nam': [metadata[0]["trackCensoredName"]],
'\xa9day': [metadata[0]["releaseDate"]],
'cprt': [extra_metadata['copyright']],
'\xa9gen': [getattr(music_video_genres, f'ID{extra_metadata["genres"][0]["genreId"]}')],
'stik': [6],
'atID': [metadata[0]['artistId']],
'cnID': [metadata[0]["trackId"]],
'geID': [int(extra_metadata['genres'][0]['genreId'])],
'sfID': [int(self.storefront.split('-')[0])],
'covr': [MP4Cover(requests.get(metadata[0]["artworkUrl30"].replace('30x30bb.jpg', '600x600bb.jpg')).content, MP4Cover.FORMAT_JPEG)]
}
if metadata[0]['trackExplicitness'] == 'notExplicit':
tags['rtng'] = [0]
elif metadata[0]['trackExplicitness'] == 'explicit':
tags['rtng'] = [1]
else:
tags['rtng'] = [2]
if len(metadata) > 1:
tags['\xa9alb'] = [metadata[1]["collectionCensoredName"]]
tags['aART'] = [metadata[1]["artistName"]]
tags['plID'] = [metadata[1]["collectionId"]]
tags['disk'] = [(metadata[0]["discNumber"], metadata[0]["discCount"])]
tags['trkn'] = [(metadata[0]["trackNumber"], metadata[0]["trackCount"])]
return tags
def get_sanizated_string(self, dirty_string, is_folder = False):
illegal_characters = ['\\', '/', ':', '*', '?', '"', '<', '>', '|', ';']
for character in illegal_characters:
dirty_string = dirty_string.replace(character, '_')
if is_folder:
if dirty_string[-1:] == '.':
dirty_string = dirty_string[:-1] + '_'
dirty_string = dirty_string[:40]
else:
dirty_string = dirty_string[:36]
return dirty_string.strip()
def get_final_location(self, file_extension, tags):
final_location = Path(self.final_path)
if 'plID' in tags.keys():
if 'cpil' in tags.keys() and tags['cpil']:
final_location /= f'Compilations/{self.get_sanizated_string(tags["©alb"][0], True)}'
else:
final_location /= f'{self.get_sanizated_string(tags["aART"][0], True)}/{self.get_sanizated_string(tags["©alb"][0], True)}'
if tags['disk'][0][1] > 1:
filename = self.get_sanizated_string(f'{tags["disk"][0][0]}-{tags["trkn"][0][0]:02d} {tags["©nam"][0]}')
else:
filename = self.get_sanizated_string(f'{tags["trkn"][0][0]:02d} {tags["©nam"][0]}')
else:
filename = self.get_sanizated_string(tags["©nam"][0])
final_location /= f'{self.get_sanizated_string(tags["©ART"][0], True)}/Unknown Album/'
final_location /= f'{filename}{file_extension}'
return final_location
def fixup_music_video(self, decrypted_location_audio, decrypted_location_video, final_location):
os.makedirs(final_location.parents[0], exist_ok = True)
os.system(f'MP4Box -quiet -add {decrypted_location_audio} -add {decrypted_location_video} -itags title=placeholder -new "{final_location}"')
def fixup_song(self, decrypted_location, final_location):
os.makedirs(final_location.parents[0], exist_ok = True)
os.system(f'MP4Box -quiet -add {decrypted_location} -itags title=placeholder -new "{final_location}"')
def make_lrc(self, final_download_location, lyrics):
with open(final_download_location.with_suffix('.lrc'), 'w', encoding = 'utf8') as f:
f.write(lyrics[1])
def apply_tags(self, final_download_location, tags):
file = MP4(final_download_location).tags
for key, value in tags.items():
file[key] = value
file.save(final_download_location)
if __name__ == '__main__':
if not shutil.which('mp4decrypt'):
print('mp4decrypt is not on PATH.')
exit(1)
if not shutil.which('MP4Box'):
print('MP4Box is not on PATH.')
exit(1)
parser = ArgumentParser(description = 'A Python script to download Apple Music albums/music videos/playlists/songs.')
parser.add_argument(
'url',
help='Apple Music albums/music videso/playlists/songs URL',
nargs='+',
metavar='<url>'
)
parser.add_argument(
'-d',
'--final-path',
default = 'Apple Music',
help = 'Set Final Path.',
metavar = '<final path>'
)
parser.add_argument(
'-a',
'--auth-path',
default = 'login',
help = 'Set Auth Path.',
metavar = '<auth path>'
)
parser.add_argument(
'-m',
'--disable-music-video-skip',
action = 'store_true',
help = 'Disable music video skip on playlists/albums.'
)
parser.add_argument(
'-p',
'--prefer-hevc',
action = 'store_true',
help = 'Prefer HEVC over AVC.'
)
parser.add_argument(
'-n',
'--no-lrc',
action = 'store_true',
help = "Don't create .lrc file."
)
parser.add_argument(
'-t',
'--temp-path',
default = 'temp',
help = 'Set Temp Path.',
metavar = '<temp path>'
)
parser.add_argument(
'-s',
'--skip-cleanup',
action = 'store_true',
help = 'Skip cleanup.'
)
parser.add_argument(
'-e',
'--print-exception',
action = 'store_true',
help = 'Print Execeptions.'
)
parser.add_argument(
'-u',
'--urls-txt',
action = 'store_true',
help = 'Use urls.txt to download URLs.'
)
args = parser.parse_args()
if not args.url and not args.download_txt:
parser.error('the following arguments are required: <url>')
if args.urls_txt:
with open('urls.txt', 'r', encoding = 'utf8') as f:
args.url = f.read().splitlines()
gamdl = Gamdl(args.disable_music_video_skip, args.auth_path, args.temp_path, args.prefer_hevc, args.final_path)
error_count = 0
for i in range(len(args.url)):
try:
download_queue = gamdl.get_download_queue(args.url[i])
except KeyboardInterrupt:
exit(0)
except:
print(f'* Failed to check URL ({i + 1} of {len(args.url)}).')
error_count += 1
continue
for j in range(len(download_queue)):
track_id = download_queue[j]['track_id']
print(f'Downloading "{download_queue[j]["title"]}" ({j + 1} of {len(download_queue)})...')
try:
webplayback = gamdl.get_webplayback(track_id)
if len(webplayback['assets']) == 0:
playlist = gamdl.get_playlist_music_video(webplayback)
stream_url_audio = gamdl.get_stream_url_music_video_audio(playlist)
encrypted_location_audio = gamdl.get_encrypted_location_audio(track_id)
gamdl.download(encrypted_location_audio, stream_url_audio)
decrypted_location_audio = gamdl.get_decrypted_location_audio(track_id)
gamdl.decrypt_music_video(decrypted_location_audio, encrypted_location_audio, stream_url_audio, track_id)
stream_url_video = gamdl.get_stream_url_music_video_video(playlist)
encrypted_location_video = gamdl.get_encrypted_location_video(track_id)
gamdl.download(encrypted_location_video, stream_url_video)
decrypted_location_video = gamdl.get_decrypted_location_video(track_id)
gamdl.decrypt_music_video(decrypted_location_video, encrypted_location_video, stream_url_video, track_id)
tags = gamdl.get_tags_music_video(download_queue[j]['alt_track_id'])
final_download_location = gamdl.get_final_location('.m4v', tags)
gamdl.fixup_music_video(decrypted_location_audio, decrypted_location_video, final_download_location)
else:
stream_url = gamdl.get_stream_url_song(webplayback)
encrypted_location = gamdl.get_encrypted_location_audio(track_id)
gamdl.download(encrypted_location, stream_url)
decrypted_location = gamdl.get_decrypted_location_audio(track_id)
gamdl.decrypt_song(decrypted_location, encrypted_location, stream_url, track_id)
lyrics = gamdl.get_lyrics(track_id)
tags = gamdl.get_tags_song(webplayback, lyrics)
final_download_location = gamdl.get_final_location('.m4a', tags)
gamdl.fixup_song(decrypted_location, final_download_location)
if not args.no_lrc and lyrics and lyrics[1]:
gamdl.make_lrc(final_download_location, lyrics)
gamdl.apply_tags(final_download_location, tags)
if not args.skip_cleanup and os.path.exists(args.temp_path):
shutil.rmtree(args.temp_path)
except KeyboardInterrupt:
exit(0)
except:
error_count += 1
print(f'* Failed to dowload "{download_queue[j]["title"]}" ({j + 1} of {len(download_queue)}).')
if args.print_exception:
traceback.print_exc()
print(f'All done ({error_count} error(s)).')
+433
View File
@@ -0,0 +1,433 @@
ID31 = "Music Videos"
ID1904 = "African"
ID2037 = "African Dancehall"
ID2038 = "African Reggae"
ID1710 = "Afrikaans"
ID1976 = "Afro House"
ID1977 = "Afro Soul"
ID1905 = "Afro-Beat"
ID1906 = "Afro-Pop"
ID2035 = "Afro-folk"
ID2036 = "Afro-fusion"
ID1978 = "Afrobeats"
ID2033 = "Alte"
ID2034 = "Amapiano"
ID1979 = "Benga"
ID1980 = "Bongo-Flava"
ID1981 = "Coupé-Décalé"
ID1982 = "Gqom"
ID1983 = "Highlife"
ID1985 = "Kizomba"
ID1984 = "Kuduro"
ID1986 = "Kwaito"
ID2000 = "Maskandi"
ID1987 = "Mbalax"
ID1988 = "Ndombolo"
ID1989 = "Shangaan Electro"
ID1990 = "Soukous"
ID1991 = "Taarab"
ID1992 = "Zouglou"
ID1620 = "Alternative"
ID1635 = "Chinese Alt"
ID1731 = "College Rock"
ID1945 = "EMO"
ID1732 = "Goth Rock"
ID1733 = "Grunge"
ID2025 = "Indie Egyptian"
ID2024 = "Indie Levant"
ID2026 = "Indie Maghreb"
ID1947 = "Indie Pop"
ID1734 = "Indie Rock"
ID1636 = "Korean Indie"
ID1735 = "New Wave"
ID1946 = "Pop Punk"
ID1736 = "Punk"
ID1998 = "Turkish Alternative"
ID1629 = "Anime"
ID1713 = "Arabic"
ID1716 = "Arabic Pop"
ID1717 = "Islamic"
ID1714 = "Khaleeji"
ID2009 = "Khaleeji Jalsat"
ID2010 = "Khaleeji Shailat"
ID2006 = "Levant"
ID2007 = "Dabke"
ID2008 = "Maghreb Rai"
ID1715 = "North African"
ID1602 = "Blues"
ID1737 = "Acoustic Blues"
ID1738 = "Chicago Blues"
ID1739 = "Classic Blues"
ID1740 = "Contemporary Blues"
ID1741 = "Country Blues"
ID1742 = "Delta Blues"
ID1743 = "Electric Blues"
ID1671 = "Brazilian"
ID1672 = "Axé"
ID1673 = "Baile Funk"
ID1674 = "Bossa Nova"
ID1675 = "Choro"
ID1676 = "Forro"
ID1677 = "Frevo"
ID1678 = "MPB"
ID1679 = "Pagode"
ID1680 = "Samba"
ID1681 = "Sertanejo"
ID1604 = "Children's Music"
ID1744 = "Lullabies"
ID1745 = "Sing-Along"
ID1746 = "Stories"
ID1637 = "Chinese"
ID1638 = "Chinese Classical"
ID1639 = "Chinese Flute"
ID1640 = "Chinese Opera"
ID1641 = "Chinese Orchestral"
ID1642 = "Chinese Regional Folk"
ID1643 = "Chinese Strings"
ID1644 = "Taiwanese Folk"
ID1645 = "Tibetan Native Music"
ID1622 = "Christian"
ID1747 = "CCM"
ID1748 = "Christian Metal"
ID1749 = "Christian Pop"
ID1750 = "Christian Rap"
ID1751 = "Christian Rock"
ID1752 = "Classic Christian"
ID1753 = "Contemporary Gospel"
ID1754 = "Gospel"
ID1755 = "Praise & Worship"
ID1756 = "Southern Gospel"
ID1757 = "Traditional Gospel"
ID1605 = "Classical"
ID1928 = "Art Song"
ID1758 = "Avant-Garde"
ID1759 = "Baroque Era"
ID1929 = "Brass & Woodwinds"
ID1933 = "Cantata"
ID1939 = "Cello"
ID1760 = "Chamber Music"
ID1761 = "Chant"
ID1762 = "Choral"
ID1763 = "Classical Crossover"
ID1682 = "High Classical"
ID1931 = "Contemporary Era"
ID1764 = "Early Music"
ID1934 = "Electronic"
ID1936 = "Guitar"
ID1765 = "Impressionist"
ID1766 = "Medieval Era"
ID1767 = "Minimalism"
ID1768 = "Modern Era"
ID1609 = "Opera"
ID1932 = "Oratorio"
ID1769 = "Orchestral"
ID1940 = "Percussion"
ID1337 = "Piano"
ID1770 = "Renaissance"
ID1771 = "Romantic Era"
ID1935 = "Sacred"
ID1930 = "Solo Instrumental"
ID1938 = "Violin"
ID1772 = "Wedding Music"
ID1603 = "Comedy"
ID1773 = "Novelty"
ID1774 = "Standup Comedy"
ID1606 = "Country"
ID1775 = "Alternative Country"
ID1776 = "Americana"
ID1777 = "Bluegrass"
ID1778 = "Contemporary Bluegrass"
ID1779 = "Contemporary Country"
ID1780 = "Country Gospel"
ID1781 = "Honky Tonk"
ID1782 = "Outlaw Country"
ID1724 = "Thai Country"
ID1783 = "Traditional Bluegrass"
ID1784 = "Traditional Country"
ID1785 = "Urban Cowboy"
ID1951 = "Cuban"
ID1956 = "Bolero"
ID1953 = "Chachacha"
ID1954 = "Guajira"
ID1957 = "Guaracha"
ID1952 = "Mambo"
ID1955 = "Son"
ID1958 = "Timba"
ID1617 = "Dance"
ID1786 = "Breakbeat"
ID1787 = "Exercise"
ID1788 = "Garage"
ID1789 = "Hardcore"
ID1790 = "House"
ID1791 = "Jungle/Drum'n'bass"
ID2032 = "Maghreb Dance"
ID1792 = "Techno"
ID1793 = "Trance"
ID1631 = "Disney"
ID1625 = "Easy Listening"
ID1794 = "Lounge"
ID1795 = "Swing"
ID1607 = "Electronic"
ID1796 = "Ambient"
ID1942 = "Bass"
ID1797 = "Downtempo"
ID1941 = "Dubstep"
ID2028 = "Electro-Cha'abi"
ID1798 = "Electronica"
ID1799 = "IDM/Experimental"
ID1800 = "Industrial"
ID2027 = "Levant Electronic"
ID2029 = "Maghreb Electronic"
ID1628 = "Enka"
ID1683 = "Fitness & Workout"
ID1719 = "Folk"
ID2030 = "Iraqi Folk"
ID2031 = "Khaleeji Folk"
ID1632 = "French Pop"
ID1634 = "German Folk"
ID1633 = "German Pop"
ID1618 = "Hip-Hop/Rap"
ID1801 = "Alternative Rap"
ID1646 = "Chinese Hip-Hop"
ID1802 = "Dirty South"
ID1803 = "East Coast Rap"
ID2021 = "Egyptian Hip-Hop"
ID1804 = "Gangsta Rap"
ID1805 = "Hardcore Rap"
ID1806 = "Hip-Hop"
ID2023 = "Khaleeji Hip-Hop"
ID1647 = "Korean Hip-Hop"
ID1807 = "Latin Rap"
ID2020 = "Levant Hip-Hop"
ID2022 = "Maghreb Hip-Hop"
ID1808 = "Old School Rap"
ID1809 = "Rap"
ID2005 = "Hip-Hop in Russian"
ID1999 = "Turkish Hip-Hop/Rap"
ID1943 = "UK Hip-Hop"
ID1810 = "Underground Rap"
ID1811 = "West Coast Rap"
ID1608 = "Holiday"
ID1812 = "Chanukah"
ID1813 = "Christmas"
ID1814 = "Christmas: Children's"
ID1815 = "Christmas: Classic"
ID1816 = "Christmas: Classical"
ID2039 = "Christmas: Country"
ID1817 = "Christmas: Jazz"
ID1818 = "Christmas: Modern"
ID1819 = "Christmas: Pop"
ID1820 = "Christmas: R&B"
ID1821 = "Christmas: Religious"
ID1822 = "Christmas: Rock"
ID1823 = "Easter"
ID1824 = "Halloween"
ID1825 = "Thanksgiving"
ID2040 = "Hörspiele"
ID1690 = "Indian"
ID1691 = "Bollywood"
ID1695 = "Devotional & Spiritual"
ID1707 = "Ghazals"
ID1697 = "Indian Classical"
ID1974 = "Carnatic Classical"
ID1975 = "Hindustani Classical"
ID1708 = "Indian Folk"
ID1704 = "Indian Pop"
ID1694 = "Regional Indian"
ID1966 = "Assamese"
ID1973 = "Bengali"
ID1961 = "Rabindra Sangeet"
ID1967 = "Bhojpuri"
ID1965 = "Gujarati"
ID1968 = "Haryanvi"
ID1963 = "Kannada"
ID1962 = "Malayalam"
ID1964 = "Marathi"
ID1969 = "Odia"
ID1972 = "Punjabi"
ID1960 = "Punjabi Pop"
ID1970 = "Rajasthani"
ID1692 = "Tamil"
ID1693 = "Telugu"
ID1971 = "Urdu"
ID1696 = "Sufi"
ID1926 = "Inspirational"
ID1684 = "Instrumental"
ID1627 = "J-Pop"
ID1611 = "Jazz"
ID1826 = "Avant-Garde Jazz"
ID1828 = "Bop"
ID1685 = "Big Band"
ID1829 = "Contemporary Jazz"
ID1830 = "Cool Jazz"
ID1831 = "Crossover Jazz"
ID1832 = "Dixieland"
ID1833 = "Fusion"
ID1834 = "Hard Bop"
ID1835 = "Latin Jazz"
ID1836 = "Mainstream Jazz"
ID1837 = "Ragtime"
ID1838 = "Smooth Jazz"
ID1839 = "Trad Jazz"
ID1902 = "Vocal Jazz"
ID1687 = "Karaoke"
ID1630 = "Kayokyoku"
ID1648 = "Korean"
ID1651 = "Korean Trad Instrumental"
ID1650 = "Korean Trad Song"
ID1652 = "Korean Trad Theater"
ID1649 = "Korean Classical"
ID1612 = "Latin"
ID1840 = "Alternative & Rock in Spanish"
ID1841 = "Baladas y Boleros"
ID1842 = "Contemporary Latin"
ID1843 = "Latin Jazz"
ID1844 = "Latin Urban"
ID1845 = "Pop in Spanish"
ID1846 = "Raices"
ID1847 = "Música Mexicana"
ID1848 = "Salsa y Tropical"
ID1721 = "Marching"
ID1613 = "New Age"
ID1849 = "Healing"
ID1850 = "Meditation"
ID1851 = "Nature"
ID1852 = "Relaxation"
ID1853 = "Travel"
ID1948 = "Yoga"
ID1720 = "Orchestral"
ID1626 = "Podcasts"
ID1614 = "Pop"
ID1854 = "Adult Contemporary"
ID1855 = "Britpop"
ID1655 = "C-Pop"
ID1656 = "Cantopop/HK-Pop"
ID2017 = "Egyptian Pop"
ID1664 = "Indo Pop"
ID2016 = "Iraqi Pop"
ID1686 = "K-Pop"
ID2019 = "Khaleeji Pop"
ID1657 = "Korean Folk-Pop"
ID2015 = "Levant Pop"
ID2018 = "Maghreb Pop"
ID1660 = "Malaysian Pop"
ID1658 = "Mandopop"
ID1663 = "Manilla Sound"
ID1723 = "Oldies"
ID1662 = "Original Pilipino Music"
ID1661 = "Pinoy Pop"
ID1856 = "Pop/Rock"
ID2003 = "Pop in Russian"
ID1950 = "Shows"
ID1857 = "Soft Rock"
ID1659 = "Tai-Pop"
ID1858 = "Teen Pop"
ID1665 = "Thai Pop"
ID1949 = "Tribute"
ID1996 = "Turkish Pop"
ID1615 = "R&B/Soul"
ID1859 = "Contemporary R&B"
ID1860 = "Disco"
ID1861 = "Doo Wop"
ID1862 = "Funk"
ID1863 = "Motown"
ID1864 = "Neo-Soul"
ID1865 = "Soul"
ID1624 = "Reggae"
ID1867 = "Dub"
ID1944 = "Lovers Rock"
ID1866 = "Modern Dancehall"
ID1868 = "Roots Reggae"
ID1869 = "Ska"
ID1621 = "Rock"
ID1870 = "Adult Alternative"
ID1871 = "American Trad Rock"
ID1872 = "Arena Rock"
ID1873 = "Blues-Rock"
ID1874 = "British Invasion"
ID1653 = "Chinese Rock"
ID1875 = "Death Metal/Black Metal"
ID1876 = "Glam Rock"
ID1877 = "Hair Metal"
ID1878 = "Hard Rock"
ID1688 = "Heavy Metal"
ID1879 = "Jam Bands"
ID1654 = "Korean Rock"
ID1880 = "Prog-Rock/Art Rock"
ID1881 = "Psychedelic"
ID1882 = "Rock & Roll"
ID1883 = "Rockabilly"
ID1884 = "Roots Rock"
ID2004 = "Rock in Russian"
ID1885 = "Singer/Songwriter"
ID1886 = "Southern Rock"
ID1887 = "Surf"
ID1888 = "Tex-Mex"
ID1997 = "Turkish Rock"
ID1729 = "Russian"
ID2002 = "Bard in Russian"
ID1698 = "Chanson in Russian"
ID2001 = "Romance in Russian"
ID1610 = "Singer/Songwriter"
ID1889 = "Alternative Folk"
ID1890 = "Contemporary Folk"
ID1891 = "Contemporary Singer/Songwriter"
ID1892 = "Folk-Rock"
ID1893 = "New Acoustic"
ID1894 = "Traditional Folk"
ID1616 = "Soundtrack"
ID1895 = "Foreign Cinema"
ID1896 = "Musicals"
ID1897 = "Original Score"
ID1718 = "Sound Effects"
ID1898 = "Soundtrack"
ID1899 = "TV Soundtrack"
ID1959 = "Video Game"
ID1689 = "Spoken Word"
ID2011 = "Tarab"
ID2013 = "Egyptian Tarab"
ID2012 = "Iraqi Tarab"
ID2014 = "Khaleeji Tarab"
ID1730 = "Turkish"
ID1709 = "Arabesque"
ID1994 = "Fantezi"
ID1700 = "Halk"
ID1995 = "Religious"
ID1701 = "Sanat"
ID1993 = "Özgün"
ID1623 = "Vocal"
ID1900 = "Standards"
ID1901 = "Traditional Pop"
ID1666 = "Trot"
ID1903 = "Vocal Pop"
ID1619 = "World"
ID1907 = "Asia"
ID1908 = "Australia"
ID1909 = "Cajun"
ID1705 = "Calypso"
ID1910 = "Caribbean"
ID1911 = "Celtic"
ID1912 = "Celtic Folk"
ID1913 = "Contemporary Celtic"
ID1702 = "Dangdut"
ID1699 = "Dini"
ID1914 = "Europe"
ID1727 = "Fado"
ID1711 = "Farsi"
ID1725 = "Flamenco"
ID1915 = "France"
ID1916 = "Hawaii"
ID1728 = "Iberia"
ID1703 = "Indonesian Religious"
ID1712 = "Israeli"
ID1917 = "Japan"
ID1918 = "Klezmer"
ID1919 = "North America"
ID1920 = "Polka"
ID1706 = "Soca"
ID1921 = "South Africa"
ID1922 = "South America"
ID1726 = "Tango"
ID1923 = "Traditional Celtic"
ID1924 = "Worldbeat"
ID1925 = "Zydeco"
View File
View File
+362
View File
@@ -0,0 +1,362 @@
import base64
import os
import time
import binascii
from google.protobuf.message import DecodeError
from google.protobuf import text_format
from pywidevine.L3.cdm.formats import wv_proto2_pb2 as wv_proto2
from pywidevine.L3.cdm.session import Session
from pywidevine.L3.cdm.key import Key
from Cryptodome.Random import get_random_bytes
from Cryptodome.Random import random
from Cryptodome.Cipher import PKCS1_OAEP, AES
from Cryptodome.Hash import CMAC, SHA256, HMAC, SHA1
from Cryptodome.PublicKey import RSA
from Cryptodome.Signature import pss
from Cryptodome.Util import Padding
import logging
class Cdm:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.sessions = {}
def open_session(self, init_data_b64, device, raw_init_data = None, offline=False):
self.logger.debug("open_session(init_data_b64={}, device={}".format(init_data_b64, device))
self.logger.info("opening new cdm session")
if device.session_id_type == 'android':
# format: 16 random hexdigits, 2 digit counter, 14 0s
rand_ascii = ''.join(random.choice('ABCDEF0123456789') for _ in range(16))
counter = '01' # this resets regularly so its fine to use 01
rest = '00000000000000'
session_id = rand_ascii + counter + rest
session_id = session_id.encode('ascii')
elif device.session_id_type == 'chrome':
rand_bytes = get_random_bytes(16)
session_id = rand_bytes
else:
# other formats NYI
self.logger.error("device type is unusable")
return 1
if raw_init_data and isinstance(raw_init_data, (bytes, bytearray)):
# used for NF key exchange, where they don't provide a valid PSSH
init_data = raw_init_data
self.raw_pssh = True
else:
init_data = self._parse_init_data(init_data_b64)
self.raw_pssh = False
if init_data:
new_session = Session(session_id, init_data, device, offline)
else:
self.logger.error("unable to parse init data")
return 1
self.sessions[session_id] = new_session
self.logger.info("session opened and init data parsed successfully")
return session_id
def _parse_init_data(self, init_data_b64):
parsed_init_data = wv_proto2.WidevineCencHeader()
try:
self.logger.debug("trying to parse init_data directly")
parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:])
except DecodeError:
self.logger.debug("unable to parse as-is, trying with removed pssh box header")
try:
id_bytes = parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:])
except DecodeError:
self.logger.error("unable to parse, unsupported init data format")
return None
self.logger.debug("init_data:")
for line in text_format.MessageToString(parsed_init_data).splitlines():
self.logger.debug(line)
return parsed_init_data
def close_session(self, session_id):
self.logger.debug("close_session(session_id={})".format(session_id))
self.logger.info("closing cdm session")
if session_id in self.sessions:
self.sessions.pop(session_id)
self.logger.info("cdm session closed")
return 0
else:
self.logger.info("session {} not found".format(session_id))
return 1
def set_service_certificate(self, session_id, cert_b64):
self.logger.debug("set_service_certificate(session_id={}, cert={})".format(session_id, cert_b64))
self.logger.info("setting service certificate")
if session_id not in self.sessions:
self.logger.error("session id doesn't exist")
return 1
session = self.sessions[session_id]
message = wv_proto2.SignedMessage()
try:
message.ParseFromString(base64.b64decode(cert_b64))
except DecodeError:
self.logger.error("failed to parse cert as SignedMessage")
service_certificate = wv_proto2.SignedDeviceCertificate()
if message.Type:
self.logger.debug("service cert provided as signedmessage")
try:
service_certificate.ParseFromString(message.Msg)
except DecodeError:
self.logger.error("failed to parse service certificate")
return 1
else:
self.logger.debug("service cert provided as signeddevicecertificate")
try:
service_certificate.ParseFromString(base64.b64decode(cert_b64))
except DecodeError:
self.logger.error("failed to parse service certificate")
return 1
self.logger.debug("service certificate:")
for line in text_format.MessageToString(service_certificate).splitlines():
self.logger.debug(line)
session.service_certificate = service_certificate
session.privacy_mode = True
return 0
def get_license_request(self, session_id):
self.logger.debug("get_license_request(session_id={})".format(session_id))
self.logger.info("getting license request")
if session_id not in self.sessions:
self.logger.error("session ID does not exist")
return 1
session = self.sessions[session_id]
# raw pssh will be treated as bytes and not parsed
if self.raw_pssh:
license_request = wv_proto2.SignedLicenseRequestRaw()
else:
license_request = wv_proto2.SignedLicenseRequest()
client_id = wv_proto2.ClientIdentification()
if not os.path.exists(session.device_config.device_client_id_blob_filename):
self.logger.error("no client ID blob available for this device")
return 1
with open(session.device_config.device_client_id_blob_filename, "rb") as f:
try:
cid_bytes = client_id.ParseFromString(f.read())
except DecodeError:
self.logger.error("client id failed to parse as protobuf")
return 1
self.logger.debug("building license request")
if not self.raw_pssh:
license_request.Type = wv_proto2.SignedLicenseRequest.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh.CopyFrom(session.init_data)
else:
license_request.Type = wv_proto2.SignedLicenseRequestRaw.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh = session.init_data # bytes
if session.offline:
license_type = wv_proto2.LicenseType.Value('OFFLINE')
else:
license_type = wv_proto2.LicenseType.Value('DEFAULT')
license_request.Msg.ContentId.CencId.LicenseType = license_type
license_request.Msg.ContentId.CencId.RequestId = session_id
license_request.Msg.Type = wv_proto2.LicenseRequest.RequestType.Value('NEW')
license_request.Msg.RequestTime = int(time.time())
license_request.Msg.ProtocolVersion = wv_proto2.ProtocolVersion.Value('CURRENT')
if session.device_config.send_key_control_nonce:
license_request.Msg.KeyControlNonce = random.randrange(1, 2**31)
if session.privacy_mode:
if session.device_config.vmp:
self.logger.debug("vmp required, adding to client_id")
self.logger.debug("reading vmp hashes")
vmp_hashes = wv_proto2.FileHashes()
with open(session.device_config.device_vmp_blob_filename, "rb") as f:
try:
vmp_bytes = vmp_hashes.ParseFromString(f.read())
except DecodeError:
self.logger.error("vmp hashes failed to parse as protobuf")
return 1
client_id._FileHashes.CopyFrom(vmp_hashes)
self.logger.debug("privacy mode & service certificate loaded, encrypting client id")
self.logger.debug("unencrypted client id:")
for line in text_format.MessageToString(client_id).splitlines():
self.logger.debug(line)
cid_aes_key = get_random_bytes(16)
cid_iv = get_random_bytes(16)
cid_cipher = AES.new(cid_aes_key, AES.MODE_CBC, cid_iv)
encrypted_client_id = cid_cipher.encrypt(Padding.pad(client_id.SerializeToString(), 16))
service_public_key = RSA.importKey(session.service_certificate._DeviceCertificate.PublicKey)
service_cipher = PKCS1_OAEP.new(service_public_key)
encrypted_cid_key = service_cipher.encrypt(cid_aes_key)
encrypted_client_id_proto = wv_proto2.EncryptedClientIdentification()
encrypted_client_id_proto.ServiceId = session.service_certificate._DeviceCertificate.ServiceId
encrypted_client_id_proto.ServiceCertificateSerialNumber = session.service_certificate._DeviceCertificate.SerialNumber
encrypted_client_id_proto.EncryptedClientId = encrypted_client_id
encrypted_client_id_proto.EncryptedClientIdIv = cid_iv
encrypted_client_id_proto.EncryptedPrivacyKey = encrypted_cid_key
license_request.Msg.EncryptedClientId.CopyFrom(encrypted_client_id_proto)
else:
license_request.Msg.ClientId.CopyFrom(client_id)
if session.device_config.private_key_available:
key = RSA.importKey(open(session.device_config.device_private_key_filename).read())
session.device_key = key
else:
self.logger.error("need device private key, other methods unimplemented")
return 1
self.logger.debug("signing license request")
hash = SHA1.new(license_request.Msg.SerializeToString())
signature = pss.new(key).sign(hash)
license_request.Signature = signature
session.license_request = license_request
self.logger.debug("license request:")
for line in text_format.MessageToString(session.license_request).splitlines():
self.logger.debug(line)
self.logger.info("license request created")
self.logger.debug("license request b64: {}".format(base64.b64encode(license_request.SerializeToString())))
return license_request.SerializeToString()
def provide_license(self, session_id, license_b64):
self.logger.debug("provide_license(session_id={}, license_b64={})".format(session_id, license_b64))
self.logger.info("decrypting provided license")
if session_id not in self.sessions:
self.logger.error("session does not exist")
return 1
session = self.sessions[session_id]
if not session.license_request:
self.logger.error("generate a license request first!")
return 1
license = wv_proto2.SignedLicense()
try:
license.ParseFromString(base64.b64decode(license_b64))
except DecodeError:
self.logger.error("unable to parse license - check protobufs")
return 1
session.license = license
self.logger.debug("license:")
for line in text_format.MessageToString(license).splitlines():
self.logger.debug(line)
self.logger.debug("deriving keys from session key")
oaep_cipher = PKCS1_OAEP.new(session.device_key)
session.session_key = oaep_cipher.decrypt(license.SessionKey)
lic_req_msg = session.license_request.Msg.SerializeToString()
enc_key_base = b"ENCRYPTION\000" + lic_req_msg + b"\0\0\0\x80"
auth_key_base = b"AUTHENTICATION\0" + lic_req_msg + b"\0\0\2\0"
enc_key = b"\x01" + enc_key_base
auth_key_1 = b"\x01" + auth_key_base
auth_key_2 = b"\x02" + auth_key_base
auth_key_3 = b"\x03" + auth_key_base
auth_key_4 = b"\x04" + auth_key_base
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(enc_key)
enc_cmac_key = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_1)
auth_cmac_key_1 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_2)
auth_cmac_key_2 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_3)
auth_cmac_key_3 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_4)
auth_cmac_key_4 = cmac_obj.digest()
auth_cmac_combined_1 = auth_cmac_key_1 + auth_cmac_key_2
auth_cmac_combined_2 = auth_cmac_key_3 + auth_cmac_key_4
session.derived_keys['enc'] = enc_cmac_key
session.derived_keys['auth_1'] = auth_cmac_combined_1
session.derived_keys['auth_2'] = auth_cmac_combined_2
self.logger.debug('verifying license signature')
lic_hmac = HMAC.new(session.derived_keys['auth_1'], digestmod=SHA256)
lic_hmac.update(license.Msg.SerializeToString())
self.logger.debug("calculated sig: {} actual sig: {}".format(lic_hmac.hexdigest(), binascii.hexlify(license.Signature)))
if lic_hmac.digest() != license.Signature:
self.logger.info("license signature doesn't match - writing bin so they can be debugged")
with open("original_lic.bin", "wb") as f:
f.write(base64.b64decode(license_b64))
with open("parsed_lic.bin", "wb") as f:
f.write(license.SerializeToString())
self.logger.info("continuing anyway")
self.logger.debug("key count: {}".format(len(license.Msg.Key)))
for key in license.Msg.Key:
if key.Id:
key_id = key.Id
else:
key_id = wv_proto2.License.KeyContainer.KeyType.Name(key.Type).encode('utf-8')
encrypted_key = key.Key
iv = key.Iv
type = wv_proto2.License.KeyContainer.KeyType.Name(key.Type)
cipher = AES.new(session.derived_keys['enc'], AES.MODE_CBC, iv=iv)
decrypted_key = cipher.decrypt(encrypted_key)
if type == "OPERATOR_SESSION":
permissions = []
perms = key._OperatorSessionKeyPermissions
for (descriptor, value) in perms.ListFields():
if value == 1:
permissions.append(descriptor.name)
print(permissions)
else:
permissions = []
session.keys.append(Key(key_id, type, Padding.unpad(decrypted_key, 16), permissions))
self.logger.info("decrypted all keys")
return 0
def get_keys(self, session_id):
if session_id in self.sessions:
return self.sessions[session_id].keys
else:
self.logger.error("session not found")
return 1
+53
View File
@@ -0,0 +1,53 @@
import os
device_android_generic = {
'name': 'android_generic',
'description': 'android studio cdm',
'security_level': 3,
'session_id_type': 'android',
'private_key_available': True,
'vmp': False,
'send_key_control_nonce': True
}
devices_available = [device_android_generic]
FILES_FOLDER = 'devices'
class DeviceConfig:
def __init__(self, device):
self.device_name = device['name']
self.description = device['description']
self.security_level = device['security_level']
self.session_id_type = device['session_id_type']
self.private_key_available = device['private_key_available']
self.vmp = device['vmp']
self.send_key_control_nonce = device['send_key_control_nonce']
if 'keybox_filename' in device:
self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['keybox_filename'])
else:
self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'keybox')
if 'device_cert_filename' in device:
self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_cert_filename'])
else:
self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_cert')
if 'device_private_key_filename' in device:
self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_private_key_filename'])
else:
self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_private_key')
if 'device_client_id_blob_filename' in device:
self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_client_id_blob_filename'])
else:
self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_client_id_blob')
if 'device_vmp_blob_filename' in device:
self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_vmp_blob_filename'])
else:
self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_vmp_blob')
def __repr__(self):
return "DeviceConfig(name={}, description={}, security_level={}, session_id_type={}, private_key_available={}, vmp={})".format(self.device_name, self.description, self.security_level, self.session_id_type, self.private_key_available, self.vmp)
@@ -0,0 +1,56 @@
// Copyright 2016 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
//
// This file defines Widevine Pssh Data proto format.
syntax = "proto2";
package shaka.media;
message WidevinePsshData {
enum Algorithm {
UNENCRYPTED = 0;
AESCTR = 1;
};
optional Algorithm algorithm = 1;
repeated bytes key_id = 2;
// Content provider name.
optional string provider = 3;
// A content identifier, specified by content provider.
optional bytes content_id = 4;
// The name of a registered policy to be used for this asset.
optional string policy = 6;
// Crypto period index, for media using key rotation.
optional uint32 crypto_period_index = 7;
// Optional protected context for group content. The grouped_license is a
// serialized SignedMessage.
optional bytes grouped_license = 8;
// Protection scheme identifying the encryption algorithm. Represented as one
// of the following 4CC values: 'cenc' (AES-CTR), 'cbc1' (AES-CBC),
// 'cens' (AES-CTR subsample), 'cbcs' (AES-CBC subsample).
optional uint32 protection_scheme = 9;
}
// Derived from WidevinePsshData. The JSON format of this proto is used in
// Widevine HLS DRM signaling v1.
// We cannot build JSON from WidevinePsshData as |key_id| is required to be in
// hex format, while |bytes| type is translated to base64 by JSON formatter, so
// we have to use |string| type and do hex conversion in the code.
message WidevineHeader {
repeated string key_ids = 2;
// Content provider name.
optional string provider = 3;
// A content identifier, specified by content provider.
optional bytes content_id = 4;
}
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: widevine_pssh_data.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18widevine_pssh_data.proto\x12\x0bshaka.media\"\x8f\x02\n\x10WidevinePsshData\x12:\n\talgorithm\x18\x01 \x01(\x0e\x32\'.shaka.media.WidevinePsshData.Algorithm\x12\x0e\n\x06key_id\x18\x02 \x03(\x0c\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c\x12\x0e\n\x06policy\x18\x06 \x01(\t\x12\x1b\n\x13\x63rypto_period_index\x18\x07 \x01(\r\x12\x17\n\x0fgrouped_license\x18\x08 \x01(\x0c\x12\x19\n\x11protection_scheme\x18\t \x01(\r\"(\n\tAlgorithm\x12\x0f\n\x0bUNENCRYPTED\x10\x00\x12\n\n\x06\x41\x45SCTR\x10\x01\"G\n\x0eWidevineHeader\x12\x0f\n\x07key_ids\x18\x02 \x03(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c')
_WIDEVINEPSSHDATA = DESCRIPTOR.message_types_by_name['WidevinePsshData']
_WIDEVINEHEADER = DESCRIPTOR.message_types_by_name['WidevineHeader']
_WIDEVINEPSSHDATA_ALGORITHM = _WIDEVINEPSSHDATA.enum_types_by_name['Algorithm']
WidevinePsshData = _reflection.GeneratedProtocolMessageType('WidevinePsshData', (_message.Message,), {
'DESCRIPTOR' : _WIDEVINEPSSHDATA,
'__module__' : 'widevine_pssh_data_pb2'
# @@protoc_insertion_point(class_scope:shaka.media.WidevinePsshData)
})
_sym_db.RegisterMessage(WidevinePsshData)
WidevineHeader = _reflection.GeneratedProtocolMessageType('WidevineHeader', (_message.Message,), {
'DESCRIPTOR' : _WIDEVINEHEADER,
'__module__' : 'widevine_pssh_data_pb2'
# @@protoc_insertion_point(class_scope:shaka.media.WidevineHeader)
})
_sym_db.RegisterMessage(WidevineHeader)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_WIDEVINEPSSHDATA._serialized_start=42
_WIDEVINEPSSHDATA._serialized_end=313
_WIDEVINEPSSHDATA_ALGORITHM._serialized_start=273
_WIDEVINEPSSHDATA_ALGORITHM._serialized_end=313
_WIDEVINEHEADER._serialized_start=315
_WIDEVINEHEADER._serialized_end=386
# @@protoc_insertion_point(module_scope)
+466
View File
@@ -0,0 +1,466 @@
syntax = "proto2";
// from x86 (partial), most of it from the ARM version:
message ClientIdentification {
enum TokenType {
KEYBOX = 0;
DEVICE_CERTIFICATE = 1;
REMOTE_ATTESTATION_CERTIFICATE = 2;
}
message NameValue {
required string Name = 1;
required string Value = 2;
}
message ClientCapabilities {
enum HdcpVersion {
HDCP_NONE = 0;
HDCP_V1 = 1;
HDCP_V2 = 2;
HDCP_V2_1 = 3;
HDCP_V2_2 = 4;
}
optional uint32 ClientToken = 1;
optional uint32 SessionToken = 2;
optional uint32 VideoResolutionConstraints = 3;
optional HdcpVersion MaxHdcpVersion = 4;
optional uint32 OemCryptoApiVersion = 5;
}
required TokenType Type = 1;
//optional bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one:
optional SignedDeviceCertificate Token = 2; // use this when parsing, "bytes" when building a client id blob
repeated NameValue ClientInfo = 3;
optional bytes ProviderClientToken = 4;
optional uint32 LicenseCounter = 5;
optional ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later
optional FileHashes _FileHashes = 7; // vmp blob goes here
}
message DeviceCertificate {
enum CertificateType {
ROOT = 0;
INTERMEDIATE = 1;
USER_DEVICE = 2;
SERVICE = 3;
}
required CertificateType Type = 1; // the compiled code reused this as ProvisionedDeviceInfo.WvSecurityLevel, however that is incorrect (compiler aliased it as they're both identical as a structure)
optional bytes SerialNumber = 2;
optional uint32 CreationTimeSeconds = 3;
optional bytes PublicKey = 4;
optional uint32 SystemId = 5;
optional uint32 TestDeviceDeprecated = 6; // is it bool or int?
optional bytes ServiceId = 7; // service URL for service certificates
}
// missing some references,
message DeviceCertificateStatus {
enum CertificateStatus {
VALID = 0;
REVOKED = 1;
}
optional bytes SerialNumber = 1;
optional CertificateStatus Status = 2;
optional ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated?
}
message DeviceCertificateStatusList {
optional uint32 CreationTimeSeconds = 1;
repeated DeviceCertificateStatus CertificateStatus = 2;
}
message EncryptedClientIdentification {
required string ServiceId = 1;
optional bytes ServiceCertificateSerialNumber = 2;
required bytes EncryptedClientId = 3;
required bytes EncryptedClientIdIv = 4;
required bytes EncryptedPrivacyKey = 5;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
enum LicenseType {
ZERO = 0;
DEFAULT = 1; // 1 is STREAMING/temporary license; on recent versions may go up to 3 (latest x86); it might be persist/don't persist type, unconfirmed
OFFLINE = 2;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
// this is just a guess because these globals got lost, but really, do we need more?
enum ProtocolVersion {
CURRENT = 21; // don't have symbols for this
}
message LicenseIdentification {
optional bytes RequestId = 1;
optional bytes SessionId = 2;
optional bytes PurchaseId = 3;
optional LicenseType Type = 4;
optional uint32 Version = 5;
optional bytes ProviderSessionToken = 6;
}
message License {
message Policy {
optional bool CanPlay = 1; // changed from uint32 to bool
optional bool CanPersist = 2;
optional bool CanRenew = 3;
optional uint32 RentalDurationSeconds = 4;
optional uint32 PlaybackDurationSeconds = 5;
optional uint32 LicenseDurationSeconds = 6;
optional uint32 RenewalRecoveryDurationSeconds = 7;
optional string RenewalServerUrl = 8;
optional uint32 RenewalDelaySeconds = 9;
optional uint32 RenewalRetryIntervalSeconds = 10;
optional bool RenewWithUsage = 11; // was uint32
}
message KeyContainer {
enum KeyType {
SIGNING = 1;
CONTENT = 2;
KEY_CONTROL = 3;
OPERATOR_SESSION = 4;
}
enum SecurityLevel {
SW_SECURE_CRYPTO = 1;
SW_SECURE_DECODE = 2;
HW_SECURE_CRYPTO = 3;
HW_SECURE_DECODE = 4;
HW_SECURE_ALL = 5;
}
message OutputProtection {
enum CGMS {
COPY_FREE = 0;
COPY_ONCE = 2;
COPY_NEVER = 3;
CGMS_NONE = 0x2A; // PC default!
}
optional ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away
optional CGMS CgmsFlags = 2;
}
message KeyControl {
required bytes KeyControlBlock = 1; // what is this?
required bytes Iv = 2;
}
message OperatorSessionKeyPermissions {
optional uint32 AllowEncrypt = 1;
optional uint32 AllowDecrypt = 2;
optional uint32 AllowSign = 3;
optional uint32 AllowSignatureVerify = 4;
}
message VideoResolutionConstraint {
optional uint32 MinResolutionPixels = 1;
optional uint32 MaxResolutionPixels = 2;
optional OutputProtection RequiredProtection = 3;
}
optional bytes Id = 1;
optional bytes Iv = 2;
optional bytes Key = 3;
optional KeyType Type = 4;
optional SecurityLevel Level = 5;
optional OutputProtection RequiredProtection = 6;
optional OutputProtection RequestedProtection = 7;
optional KeyControl _KeyControl = 8; // duped names, etc
optional OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc
repeated VideoResolutionConstraint VideoResolutionConstraints = 10;
}
optional LicenseIdentification Id = 1;
optional Policy _Policy = 2; // duped names, etc
repeated KeyContainer Key = 3;
optional uint32 LicenseStartTime = 4;
optional uint32 RemoteAttestationVerified = 5; // bool?
optional bytes ProviderClientToken = 6;
// there might be more, check with newer versions (I see field 7-8 in a lic)
// this appeared in latest x86:
optional uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc)
}
message LicenseError {
enum Error {
INVALID_DEVICE_CERTIFICATE = 1;
REVOKED_DEVICE_CERTIFICATE = 2;
SERVICE_UNAVAILABLE = 3;
}
//LicenseRequest.RequestType ErrorCode; // clang mismatch
optional Error ErrorCode = 1;
}
message LicenseRequest {
message ContentIdentification {
message CENC {
//optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with:
optional WidevineCencHeader Pssh = 1;
optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!)
optional bytes RequestId = 3;
}
message WebM {
optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
optional LicenseType LicenseType = 2;
optional bytes RequestId = 3;
}
message ExistingLicense {
optional LicenseIdentification LicenseId = 1;
optional uint32 SecondsSinceStarted = 2;
optional uint32 SecondsSinceLastPlayed = 3;
optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB!
}
optional CENC CencId = 1;
optional WebM WebmId = 2;
optional ExistingLicense License = 3;
}
enum RequestType {
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
optional ClientIdentification ClientId = 1;
optional ContentIdentification ContentId = 2;
optional RequestType Type = 3;
optional uint32 RequestTime = 4;
optional bytes KeyControlNonceDeprecated = 5;
optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
optional uint32 KeyControlNonce = 7;
optional EncryptedClientIdentification EncryptedClientId = 8;
}
// raw pssh hack
message LicenseRequestRaw {
message ContentIdentification {
message CENC {
optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with:
//optional WidevineCencHeader Pssh = 1;
optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!)
optional bytes RequestId = 3;
}
message WebM {
optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
optional LicenseType LicenseType = 2;
optional bytes RequestId = 3;
}
message ExistingLicense {
optional LicenseIdentification LicenseId = 1;
optional uint32 SecondsSinceStarted = 2;
optional uint32 SecondsSinceLastPlayed = 3;
optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB!
}
optional CENC CencId = 1;
optional WebM WebmId = 2;
optional ExistingLicense License = 3;
}
enum RequestType {
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
optional ClientIdentification ClientId = 1;
optional ContentIdentification ContentId = 2;
optional RequestType Type = 3;
optional uint32 RequestTime = 4;
optional bytes KeyControlNonceDeprecated = 5;
optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
optional uint32 KeyControlNonce = 7;
optional EncryptedClientIdentification EncryptedClientId = 8;
}
message ProvisionedDeviceInfo {
enum WvSecurityLevel {
LEVEL_UNSPECIFIED = 0;
LEVEL_1 = 1;
LEVEL_2 = 2;
LEVEL_3 = 3;
}
optional uint32 SystemId = 1;
optional string Soc = 2;
optional string Manufacturer = 3;
optional string Model = 4;
optional string DeviceType = 5;
optional uint32 ModelYear = 6;
optional WvSecurityLevel SecurityLevel = 7;
optional uint32 TestDevice = 8; // bool?
}
// todo: fill
message ProvisioningOptions {
}
// todo: fill
message ProvisioningRequest {
}
// todo: fill
message ProvisioningResponse {
}
message RemoteAttestation {
optional EncryptedClientIdentification Certificate = 1;
optional string Salt = 2;
optional string Signature = 3;
}
// todo: fill
message SessionInit {
}
// todo: fill
message SessionState {
}
// todo: fill
message SignedCertificateStatusList {
}
message SignedDeviceCertificate {
//optional bytes DeviceCertificate = 1; // again, they use a buffer where it's supposed to be a message, so we'll replace it with what it really is:
optional DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later
optional bytes Signature = 2;
optional SignedDeviceCertificate Signer = 3;
}
// todo: fill
message SignedProvisioningMessage {
}
// the root of all messages, from either server or client
message SignedMessage {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional bytes Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
// This message is copied from google's docs, not reversed:
message WidevineCencHeader {
enum Algorithm {
UNENCRYPTED = 0;
AESCTR = 1;
};
optional Algorithm algorithm = 1;
repeated bytes key_id = 2;
// Content provider name.
optional string provider = 3;
// A content identifier, specified by content provider.
optional bytes content_id = 4;
// Track type. Acceptable values are SD, HD and AUDIO. Used to
// differentiate content keys used by an asset.
optional string track_type_deprecated = 5;
// The name of a registered policy to be used for this asset.
optional string policy = 6;
// Crypto period index, for media using key rotation.
optional uint32 crypto_period_index = 7;
// Optional protected context for group content. The grouped_license is a
// serialized SignedMessage.
optional bytes grouped_license = 8;
// Protection scheme identifying the encryption algorithm.
// Represented as one of the following 4CC values:
// 'cenc' (AESCTR), 'cbc1' (AESCBC),
// 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample).
optional uint32 protection_scheme = 9;
// Optional. For media using key rotation, this represents the duration
// of each crypto period in seconds.
optional uint32 crypto_period_seconds = 10;
}
// remove these when using it outside of protoc:
// from here on, it's just for testing, these messages don't exist in the binaries, I'm adding them to avoid detecting type programmatically
message SignedLicenseRequest {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional LicenseRequest Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
// hack
message SignedLicenseRequestRaw {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional LicenseRequestRaw Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
message SignedLicense {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional License Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
message SignedServiceCertificate {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
optional SignedDeviceCertificate Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
//vmp support
message FileHashes {
message Signature {
optional string filename = 1;
optional bool test_signing = 2; //0 - release, 1 - testing
optional bytes SHA512Hash = 3;
optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file
optional bytes signature = 5;
}
optional bytes signer = 1;
repeated Signature signatures = 2;
}
File diff suppressed because one or more lines are too long
+389
View File
@@ -0,0 +1,389 @@
// beware proto3 won't show missing fields it seems, need to change to "proto2" and add "optional" before every field, and remove all the dummy enum members I added:
syntax = "proto3";
// from x86 (partial), most of it from the ARM version:
message ClientIdentification {
enum TokenType {
KEYBOX = 0;
DEVICE_CERTIFICATE = 1;
REMOTE_ATTESTATION_CERTIFICATE = 2;
}
message NameValue {
string Name = 1;
string Value = 2;
}
message ClientCapabilities {
enum HdcpVersion {
HDCP_NONE = 0;
HDCP_V1 = 1;
HDCP_V2 = 2;
HDCP_V2_1 = 3;
HDCP_V2_2 = 4;
}
uint32 ClientToken = 1;
uint32 SessionToken = 2;
uint32 VideoResolutionConstraints = 3;
HdcpVersion MaxHdcpVersion = 4;
uint32 OemCryptoApiVersion = 5;
}
TokenType Type = 1;
//bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one:
SignedDeviceCertificate Token = 2;
repeated NameValue ClientInfo = 3;
bytes ProviderClientToken = 4;
uint32 LicenseCounter = 5;
ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later
}
message DeviceCertificate {
enum CertificateType {
ROOT = 0;
INTERMEDIATE = 1;
USER_DEVICE = 2;
SERVICE = 3;
}
//ProvisionedDeviceInfo.WvSecurityLevel Type = 1; // is this how one is supposed to call it? (it's an enum) there might be a bug here, with CertificateType getting confused with WvSecurityLevel, for now renaming it (verify against other binaries)
CertificateType Type = 1;
bytes SerialNumber = 2;
uint32 CreationTimeSeconds = 3;
bytes PublicKey = 4;
uint32 SystemId = 5;
uint32 TestDeviceDeprecated = 6; // is it bool or int?
bytes ServiceId = 7; // service URL for service certificates
}
// missing some references,
message DeviceCertificateStatus {
enum CertificateStatus {
VALID = 0;
REVOKED = 1;
}
bytes SerialNumber = 1;
CertificateStatus Status = 2;
ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated?
}
message DeviceCertificateStatusList {
uint32 CreationTimeSeconds = 1;
repeated DeviceCertificateStatus CertificateStatus = 2;
}
message EncryptedClientIdentification {
string ServiceId = 1;
bytes ServiceCertificateSerialNumber = 2;
bytes EncryptedClientId = 3;
bytes EncryptedClientIdIv = 4;
bytes EncryptedPrivacyKey = 5;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
enum LicenseType {
ZERO = 0;
DEFAULT = 1; // do not know what this is either, but should be 1; on recent versions may go up to 3 (latest x86)
}
// todo: fill (for this top-level type, it might be impossible/difficult)
// this is just a guess because these globals got lost, but really, do we need more?
enum ProtocolVersion {
DUMMY = 0;
CURRENT = 21; // don't have symbols for this
}
message LicenseIdentification {
bytes RequestId = 1;
bytes SessionId = 2;
bytes PurchaseId = 3;
LicenseType Type = 4;
uint32 Version = 5;
bytes ProviderSessionToken = 6;
}
message License {
message Policy {
uint32 CanPlay = 1;
uint32 CanPersist = 2;
uint32 CanRenew = 3;
uint32 RentalDurationSeconds = 4;
uint32 PlaybackDurationSeconds = 5;
uint32 LicenseDurationSeconds = 6;
uint32 RenewalRecoveryDurationSeconds = 7;
string RenewalServerUrl = 8;
uint32 RenewalDelaySeconds = 9;
uint32 RenewalRetryIntervalSeconds = 10;
uint32 RenewWithUsage = 11;
uint32 UnknownPolicy12 = 12;
}
message KeyContainer {
enum KeyType {
_NOKEYTYPE = 0; // dummy, added to satisfy proto3, not present in original
SIGNING = 1;
CONTENT = 2;
KEY_CONTROL = 3;
OPERATOR_SESSION = 4;
}
enum SecurityLevel {
_NOSECLEVEL = 0; // dummy, added to satisfy proto3, not present in original
SW_SECURE_CRYPTO = 1;
SW_SECURE_DECODE = 2;
HW_SECURE_CRYPTO = 3;
HW_SECURE_DECODE = 4;
HW_SECURE_ALL = 5;
}
message OutputProtection {
enum CGMS {
COPY_FREE = 0;
COPY_ONCE = 2;
COPY_NEVER = 3;
CGMS_NONE = 0x2A; // PC default!
}
ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away
CGMS CgmsFlags = 2;
}
message KeyControl {
bytes KeyControlBlock = 1; // what is this?
bytes Iv = 2;
}
message OperatorSessionKeyPermissions {
uint32 AllowEncrypt = 1;
uint32 AllowDecrypt = 2;
uint32 AllowSign = 3;
uint32 AllowSignatureVerify = 4;
}
message VideoResolutionConstraint {
uint32 MinResolutionPixels = 1;
uint32 MaxResolutionPixels = 2;
OutputProtection RequiredProtection = 3;
}
bytes Id = 1;
bytes Iv = 2;
bytes Key = 3;
KeyType Type = 4;
SecurityLevel Level = 5;
OutputProtection RequiredProtection = 6;
OutputProtection RequestedProtection = 7;
KeyControl _KeyControl = 8; // duped names, etc
OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc
repeated VideoResolutionConstraint VideoResolutionConstraints = 10;
}
LicenseIdentification Id = 1;
Policy _Policy = 2; // duped names, etc
repeated KeyContainer Key = 3;
uint32 LicenseStartTime = 4;
uint32 RemoteAttestationVerified = 5; // bool?
bytes ProviderClientToken = 6;
// there might be more, check with newer versions (I see field 7-8 in a lic)
// this appeared in latest x86:
uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc)
bytes UnknownHdcpDataField = 8;
}
message LicenseError {
enum Error {
DUMMY_NO_ERROR = 0; // dummy, added to satisfy proto3
INVALID_DEVICE_CERTIFICATE = 1;
REVOKED_DEVICE_CERTIFICATE = 2;
SERVICE_UNAVAILABLE = 3;
}
//LicenseRequest.RequestType ErrorCode; // clang mismatch
Error ErrorCode = 1;
}
message LicenseRequest {
message ContentIdentification {
message CENC {
// bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with:
WidevineCencHeader Pssh = 1;
LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1
bytes RequestId = 3;
}
message WebM {
bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
LicenseType LicenseType = 2;
bytes RequestId = 3;
}
message ExistingLicense {
LicenseIdentification LicenseId = 1;
uint32 SecondsSinceStarted = 2;
uint32 SecondsSinceLastPlayed = 3;
bytes SessionUsageTableEntry = 4;
}
CENC CencId = 1;
WebM WebmId = 2;
ExistingLicense License = 3;
}
enum RequestType {
DUMMY_REQ_TYPE = 0; // dummy, added to satisfy proto3
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
ClientIdentification ClientId = 1;
ContentIdentification ContentId = 2;
RequestType Type = 3;
uint32 RequestTime = 4;
bytes KeyControlNonceDeprecated = 5;
ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
uint32 KeyControlNonce = 7;
EncryptedClientIdentification EncryptedClientId = 8;
}
message ProvisionedDeviceInfo {
enum WvSecurityLevel {
LEVEL_UNSPECIFIED = 0;
LEVEL_1 = 1;
LEVEL_2 = 2;
LEVEL_3 = 3;
}
uint32 SystemId = 1;
string Soc = 2;
string Manufacturer = 3;
string Model = 4;
string DeviceType = 5;
uint32 ModelYear = 6;
WvSecurityLevel SecurityLevel = 7;
uint32 TestDevice = 8; // bool?
}
// todo: fill
message ProvisioningOptions {
}
// todo: fill
message ProvisioningRequest {
}
// todo: fill
message ProvisioningResponse {
}
message RemoteAttestation {
EncryptedClientIdentification Certificate = 1;
string Salt = 2;
string Signature = 3;
}
// todo: fill
message SessionInit {
}
// todo: fill
message SessionState {
}
// todo: fill
message SignedCertificateStatusList {
}
message SignedDeviceCertificate {
//bytes DeviceCertificate = 1; // again, they use a buffer where it's supposed to be a message, so we'll replace it with what it really is:
DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later
bytes Signature = 2;
SignedDeviceCertificate Signer = 3;
}
// todo: fill
message SignedProvisioningMessage {
}
// the root of all messages, from either server or client
message SignedMessage {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
bytes Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}
// This message is copied from google's docs, not reversed:
message WidevineCencHeader {
enum Algorithm {
UNENCRYPTED = 0;
AESCTR = 1;
};
Algorithm algorithm = 1;
repeated bytes key_id = 2;
// Content provider name.
string provider = 3;
// A content identifier, specified by content provider.
bytes content_id = 4;
// Track type. Acceptable values are SD, HD and AUDIO. Used to
// differentiate content keys used by an asset.
string track_type_deprecated = 5;
// The name of a registered policy to be used for this asset.
string policy = 6;
// Crypto period index, for media using key rotation.
uint32 crypto_period_index = 7;
// Optional protected context for group content. The grouped_license is a
// serialized SignedMessage.
bytes grouped_license = 8;
// Protection scheme identifying the encryption algorithm.
// Represented as one of the following 4CC values:
// 'cenc' (AESCTR), 'cbc1' (AESCBC),
// 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample).
uint32 protection_scheme = 9;
// Optional. For media using key rotation, this represents the duration
// of each crypto period in seconds.
uint32 crypto_period_seconds = 10;
}
// from here on, it's just for testing, these messages don't exist in the binaries, I'm adding them to avoid detecting type programmatically
message SignedLicenseRequest {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
LicenseRequest Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}
message SignedLicense {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
License Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
import binascii
class Key:
def __init__(self, kid, type, key, permissions=[]):
self.kid = kid
self.type = type
self.key = key
self.permissions = permissions
def __repr__(self):
if self.type == "OPERATOR_SESSION":
return "key(kid={}, type={}, key={}, permissions={})".format(self.kid, self.type, binascii.hexlify(self.key), self.permissions)
else:
return "key(kid={}, type={}, key={})".format(self.kid, self.type, binascii.hexlify(self.key))
+18
View File
@@ -0,0 +1,18 @@
class Session:
def __init__(self, session_id, init_data, device_config, offline):
self.session_id = session_id
self.init_data = init_data
self.offline = offline
self.device_config = device_config
self.device_key = None
self.session_key = None
self.derived_keys = {
'enc': None,
'auth_1': None,
'auth_2': None
}
self.license_request = None
self.license = None
self.service_certificate = None
self.privacy_mode = False
self.keys = []
+102
View File
@@ -0,0 +1,102 @@
try:
from google.protobuf.internal.decoder import _DecodeVarint as _di # this was tested to work with protobuf 3, but it's an internal API (any varint decoder might work)
except ImportError:
# this is generic and does not depend on pb internals, however it will decode "larger" possible numbers than pb decoder which has them fixed
def LEB128_decode(buffer, pos, limit = 64):
result = 0
shift = 0
while True:
b = buffer[pos]
pos += 1
result |= ((b & 0x7F) << shift)
if not (b & 0x80):
return (result, pos)
shift += 7
if shift > limit:
raise Exception("integer too large, shift: {}".format(shift))
_di = LEB128_decode
class FromFileMixin:
@classmethod
def from_file(cls, filename):
"""Load given a filename"""
with open(filename,"rb") as f:
return cls(f.read())
# the signatures use a format internally similar to
# protobuf's encoding, but without wire types
class VariableReader(FromFileMixin):
"""Protobuf-like encoding reader"""
def __init__(self, buf):
self.buf = buf
self.pos = 0
self.size = len(buf)
def read_int(self):
"""Read a variable length integer"""
# _DecodeVarint will take care of out of range errors
(val, nextpos) = _di(self.buf, self.pos)
self.pos = nextpos
return val
def read_bytes_raw(self, size):
"""Read size bytes"""
b = self.buf[self.pos:self.pos+size]
self.pos += size
return b
def read_bytes(self):
"""Read a bytes object"""
size = self.read_int()
return self.read_bytes_raw(size)
def is_end(self):
return (self.size == self.pos)
class TaggedReader(VariableReader):
"""Tagged reader, needed for implementing a WideVine signature reader"""
def read_tag(self):
"""Read a tagged buffer"""
return (self.read_int(), self.read_bytes())
def read_all_tags(self, max_tag=3):
tags = {}
while (not self.is_end()):
(tag, bytes) = self.read_tag()
if (tag > max_tag):
raise IndexError("tag out of bound: got {}, max {}".format(tag, max_tag))
tags[tag] = bytes
return tags
class WideVineSignatureReader(FromFileMixin):
"""Parses a widevine .sig signature file."""
SIGNER_TAG = 1
SIGNATURE_TAG = 2
ISMAINEXE_TAG = 3
def __init__(self, buf):
reader = TaggedReader(buf)
self.version = reader.read_int()
if (self.version != 0):
raise Exception("Unsupported signature format version {}".format(self.version))
self.tags = reader.read_all_tags()
self.signer = self.tags[self.SIGNER_TAG]
self.signature = self.tags[self.SIGNATURE_TAG]
extra = self.tags[self.ISMAINEXE_TAG]
if (len(extra) != 1 or (extra[0] > 1)):
raise Exception("Unexpected 'ismainexe' field value (not '\\x00' or '\\x01'), please check: {0}".format(extra))
self.mainexe = bool(extra[0])
@classmethod
def get_tags(cls, filename):
"""Return a dictionary of each tag in the signature file"""
return cls.from_file(filename).tags
View File
+57
View File
@@ -0,0 +1,57 @@
import base64
import logging
import subprocess
from pywidevine.L3.cdm import cdm, deviceconfig
class WvDecrypt(object):
WV_SYSTEM_ID = [237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]
def __init__(self, config):
self.config = config
self.logger = logging.getLogger(__name__)
self.wvdecrypt_process = None
self.logger.debug(self.log_message("wvdecrypt object created"))
self.cdm = cdm.Cdm()
def check_pssh(pssh_b64):
pssh = base64.b64decode(pssh_b64)
if not pssh[12:28] == bytes(self.WV_SYSTEM_ID):
new_pssh = bytearray([0,0,0])
new_pssh.append(32+len(pssh))
new_pssh[4:] = bytearray(b'pssh')
new_pssh[8:] = [0,0,0,0]
new_pssh[13:] = self.WV_SYSTEM_ID
new_pssh[29:] = [0,0,0,0]
new_pssh[31] = len(pssh)
new_pssh[32:] = pssh
return base64.b64encode(new_pssh)
else:
return pssh_b64
self.session = self.cdm.open_session(check_pssh(config.init_data_b64),
deviceconfig.DeviceConfig(deviceconfig.device_android_generic))
self.logger.debug(self.log_message("widevine session opened"))
def log_message(self, msg):
return "{}_{} : {}".format('audio', '0', msg)
def start_process(self):
decryption_keys = self.cdm.get_keys(self.session)
self.logger.debug(self.log_message("starting process"))
self.logger.debug(self.config.build_commandline_list(decryption_keys))
self.wvdecrypt_process = subprocess.run(
self.config.build_commandline_list(decryption_keys),
check=True
)
self.logger.debug(self.log_message("decrypted successfully"))
def get_challenge(self):
return self.cdm.get_license_request(self.session)
def update_license(self, license_b64):
self.cdm.provide_license(self.session, license_b64)
return True
+17
View File
@@ -0,0 +1,17 @@
class WvDecryptConfig(object):
def __init__(self, decrypted_download_location, encrypted_download_location, init_data_b64):
self.decrypted_download_location = str(decrypted_download_location)
self.encrypted_download_location = str(encrypted_download_location)
self.init_data_b64 = init_data_b64
def build_commandline_list(self, keys):
commandline = ['mp4decrypt']
commandline.append('--show-progress')
for key in keys:
if key.type == 'CONTENT':
commandline.append('--key')
default_KID = 1
commandline.append('{}:{}'.format(str(default_KID), key.key.hex()))
commandline.append(self.encrypted_download_location)
commandline.append(self.decrypted_download_location)
return commandline
+59
View File
@@ -0,0 +1,59 @@
# uncompyle6 version 3.7.3
# Python bytecode 3.6 (3379)
# Decompiled from: Python 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)]
# Embedded file name: pywidevine\decrypt\wvdecryptcustom.py
import logging, subprocess, re, base64
from pywidevine.L1.cdm import cdm, deviceconfig
class WvDecrypt(object):
WV_SYSTEM_ID = [
237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]
def __init__(self, init_data_b64, cert_data_b64, device):
self.init_data_b64 = init_data_b64
self.cert_data_b64 = cert_data_b64
self.device = device
self.cdm = cdm.Cdm()
def check_pssh(pssh_b64):
pssh = base64.b64decode(pssh_b64)
if not pssh[12:28] == bytes(self.WV_SYSTEM_ID):
new_pssh = bytearray([0, 0, 0])
new_pssh.append(32 + len(pssh))
new_pssh[4:] = bytearray(b'pssh')
new_pssh[8:] = [0, 0, 0, 0]
new_pssh[13:] = self.WV_SYSTEM_ID
new_pssh[29:] = [0, 0, 0, 0]
new_pssh[31] = len(pssh)
new_pssh[32:] = pssh
return base64.b64encode(new_pssh)
else:
return pssh_b64
self.session = self.cdm.open_session(check_pssh(self.init_data_b64), deviceconfig.DeviceConfig(self.device))
if self.cert_data_b64:
self.cdm.set_service_certificate(self.session, self.cert_data_b64)
def log_message(self, msg):
return '{}'.format(msg)
def start_process(self):
keyswvdecrypt = []
try:
for key in self.cdm.get_keys(self.session):
if key.type == 'CONTENT':
keyswvdecrypt.append(self.log_message('{}:{}'.format(key.kid.hex(), key.key.hex())))
except Exception:
return (
False, keyswvdecrypt)
else:
return (
True, keyswvdecrypt)
def get_challenge(self):
return self.cdm.get_license_request(self.session)
def update_license(self, license_b64):
self.cdm.provide_license(self.session, license_b64)
return True
+14
View File
@@ -0,0 +1,14 @@
import requests, xmltodict, json
def get_pssh(mpd_url):
r = requests.get(url=mpd_url)
r.raise_for_status()
xml = xmltodict.parse(r.text)
mpd = json.loads(json.dumps(xml))
tracks = mpd['MPD']['Period']['AdaptationSet']
for video_tracks in tracks:
if video_tracks['@mimeType'] == 'video/mp4':
for t in video_tracks["ContentProtection"]:
if t['@schemeIdUri'].lower() == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":
pssh = t["pssh"]
return pssh
+4
View File
@@ -0,0 +1,4 @@
m3u8
google-api-python-client
yt-dlp
requests
+438
View File
@@ -0,0 +1,438 @@
ID34 = "Music"
ID1203 = "African"
ID45 = "African Dancehall"
ID46 = "African Reggae"
ID1281 = "Afrikaans"
ID100049 = "Afro House"
ID100050 = "Afro Soul"
ID1177 = "Afro-Beat"
ID1178 = "Afro-Pop"
ID43 = "Afro-folk"
ID44 = "Afro-fusion"
ID100051 = "Afrobeats"
ID41 = "Alte"
ID42 = "Amapiano"
ID100052 = "Benga"
ID100053 = "Bongo-Flava"
ID100054 = "Coupé-Décalé"
ID100055 = "Gqom"
ID100056 = "Highlife"
ID100058 = "Kizomba"
ID100057 = "Kuduro"
ID100059 = "Kwaito"
ID100073 = "Maskandi"
ID100060 = "Mbalax"
ID100061 = "Ndombolo"
ID100062 = "Shangaan Electro"
ID100063 = "Soukous"
ID100064 = "Taarab"
ID100065 = "Zouglou"
ID20 = "Alternative"
ID1230 = "Chinese Alt"
ID1001 = "College Rock"
ID100018 = "EMO"
ID1002 = "Goth Rock"
ID1003 = "Grunge"
ID100098 = "Indie Egyptian"
ID100097 = "Indie Levant"
ID100099 = "Indie Maghreb"
ID100020 = "Indie Pop"
ID1004 = "Indie Rock"
ID1231 = "Korean Indie"
ID1005 = "New Wave"
ID100019 = "Pop Punk"
ID1006 = "Punk"
ID100071 = "Turkish Alternative"
ID29 = "Anime"
ID1197 = "Arabic"
ID1286 = "Arabic Pop"
ID1287 = "Islamic"
ID1284 = "Khaleeji"
ID100082 = "Khaleeji Jalsat"
ID100083 = "Khaleeji Shailat"
ID100079 = "Levant"
ID100080 = "Dabke"
ID100081 = "Maghreb Rai"
ID1285 = "North African"
ID2 = "Blues"
ID1210 = "Acoustic Blues"
ID1007 = "Chicago Blues"
ID1009 = "Classic Blues"
ID1010 = "Contemporary Blues"
ID1011 = "Country Blues"
ID1012 = "Delta Blues"
ID1013 = "Electric Blues"
ID1122 = "Brazilian"
ID1220 = "Axé"
ID1229 = "Baile Funk"
ID1221 = "Bossa Nova"
ID1222 = "Choro"
ID1223 = "Forró"
ID1224 = "Frevo"
ID1225 = "MPB"
ID1226 = "Pagode"
ID1227 = "Samba"
ID1228 = "Sertanejo"
ID4 = "Children's Music"
ID1014 = "Lullabies"
ID1015 = "Sing-Along"
ID1016 = "Stories"
ID1232 = "Chinese"
ID1233 = "Chinese Classical"
ID1234 = "Chinese Flute"
ID1235 = "Chinese Opera"
ID1236 = "Chinese Orchestral"
ID1237 = "Chinese Regional Folk"
ID1238 = "Chinese Strings"
ID1239 = "Taiwanese Folk"
ID1240 = "Tibetan Native Music"
ID22 = "Christian"
ID1094 = "CCM"
ID1095 = "Christian Metal"
ID1096 = "Christian Pop"
ID1097 = "Christian Rap"
ID1098 = "Christian Rock"
ID1099 = "Classic Christian"
ID1100 = "Contemporary Gospel"
ID1101 = "Gospel"
ID1103 = "Praise & Worship"
ID1104 = "Southern Gospel"
ID1105 = "Traditional Gospel"
ID5 = "Classical"
ID100001 = "Art Song"
ID1017 = "Avant-Garde"
ID1018 = "Baroque Era"
ID100002 = "Brass & Woodwinds"
ID100006 = "Cantata"
ID100012 = "Cello"
ID1019 = "Chamber Music"
ID1020 = "Chant"
ID1021 = "Choral"
ID1022 = "Classical Crossover"
ID1211 = "High Classical"
ID100004 = "Contemporary Era"
ID1023 = "Early Music"
ID100007 = "Electronic"
ID100009 = "Guitar"
ID1024 = "Impressionist"
ID1025 = "Medieval Era"
ID1026 = "Minimalism"
ID1027 = "Modern Era"
ID9 = "Opera"
ID1028 = "Opera"
ID100005 = "Oratorio"
ID1029 = "Orchestral"
ID100013 = "Percussion"
ID100010 = "Piano"
ID1030 = "Renaissance"
ID1031 = "Romantic Era"
ID100008 = "Sacred"
ID100003 = "Solo Instrumental"
ID100011 = "Violin"
ID1032 = "Wedding Music"
ID3 = "Comedy"
ID1167 = "Novelty"
ID1171 = "Standup Comedy"
ID6 = "Country"
ID1033 = "Alternative Country"
ID1034 = "Americana"
ID1035 = "Bluegrass"
ID1036 = "Contemporary Bluegrass"
ID1037 = "Contemporary Country"
ID1038 = "Country Gospel"
ID1039 = "Honky Tonk"
ID1040 = "Outlaw Country"
ID1294 = "Thai Country"
ID1041 = "Traditional Bluegrass"
ID1042 = "Traditional Country"
ID1043 = "Urban Cowboy"
ID100024 = "Cuban"
ID100029 = "Bolero"
ID100026 = "Chachacha"
ID100027 = "Guajira"
ID100030 = "Guaracha"
ID100025 = "Mambo"
ID100028 = "Son"
ID100031 = "Timba"
ID17 = "Dance"
ID1044 = "Breakbeat"
ID1045 = "Exercise"
ID1046 = "Garage"
ID1047 = "Hardcore"
ID1048 = "House"
ID1049 = "Jungle/Drum'n'bass"
ID100105 = "Maghreb Dance"
ID1050 = "Techno"
ID1051 = "Trance"
ID50000063 = "Disney"
ID25 = "Easy Listening"
ID1054 = "Lounge"
ID1055 = "Swing"
ID7 = "Electronic"
ID1056 = "Ambient"
ID100015 = "Bass"
ID1057 = "Downtempo"
ID100014 = "Dubstep"
ID100101 = "Electro-Cha'abi"
ID1058 = "Electronica"
ID1060 = "IDM/Experimental"
ID1061 = "Industrial"
ID100100 = "Levant Electronic"
ID100102 = "Maghreb Electronic"
ID28 = "Enka"
ID50 = "Fitness & Workout"
ID1289 = "Folk"
ID100103 = "Iraqi Folk"
ID100104 = "Khaleeji Folk"
ID50000064 = "French Pop"
ID50000068 = "German Folk"
ID50000066 = "German Pop"
ID18 = "Hip-Hop/Rap"
ID1068 = "Alternative Rap"
ID1241 = "Chinese Hip-Hop"
ID1069 = "Dirty South"
ID1070 = "East Coast Rap"
ID100094 = "Egyptian Hip-Hop"
ID1071 = "Gangsta Rap"
ID1072 = "Hardcore Rap"
ID1073 = "Hip-Hop"
ID100096 = "Khaleeji Hip-Hop"
ID1242 = "Korean Hip-Hop"
ID1074 = "Latin Rap"
ID100093 = "Levant Hip-Hop"
ID100095 = "Maghreb Hip-Hop"
ID1075 = "Old School Rap"
ID1076 = "Rap"
ID100078 = "Hip-Hop in Russian"
ID100072 = "Turkish Hip-Hop/Rap"
ID100016 = "UK Hip-Hop"
ID1077 = "Underground Rap"
ID1078 = "West Coast Rap"
ID8 = "Holiday"
ID1079 = "Chanukah"
ID1080 = "Christmas"
ID1081 = "Christmas: Children's"
ID1082 = "Christmas: Classic"
ID1083 = "Christmas: Classical"
ID100106 = "Christmas: Country"
ID1084 = "Christmas: Jazz"
ID1085 = "Christmas: Modern"
ID1086 = "Christmas: Pop"
ID1087 = "Christmas: R&B"
ID1088 = "Christmas: Religious"
ID1089 = "Christmas: Rock"
ID1090 = "Easter"
ID1091 = "Halloween"
ID1092 = "Holiday: Other"
ID1093 = "Thanksgiving"
ID100107 = "Hörspiele"
ID1262 = "Indian"
ID1263 = "Bollywood"
ID1267 = "Devotional & Spiritual"
ID1278 = "Ghazals"
ID1269 = "Indian Classical"
ID100047 = "Carnatic Classical"
ID100048 = "Hindustani Classical"
ID1279 = "Indian Folk"
ID1185 = "Indian Pop"
ID1266 = "Regional Indian"
ID100039 = "Assamese"
ID100046 = "Bengali"
ID100034 = "Rabindra Sangeet"
ID100040 = "Bhojpuri"
ID100038 = "Gujarati"
ID100041 = "Haryanvi"
ID100036 = "Kannada"
ID100035 = "Malayalam"
ID100037 = "Marathi"
ID100042 = "Odia"
ID100045 = "Punjabi"
ID100033 = "Punjabi Pop"
ID100043 = "Rajasthani"
ID1264 = "Tamil"
ID1265 = "Telugu"
ID100044 = "Urdu"
ID1268 = "Sufi"
ID100000 = "Inspirational"
ID53 = "Instrumental"
ID27 = "J-Pop"
ID11 = "Jazz"
ID1106 = "Avant-Garde Jazz"
ID1053 = "Bop"
ID1052 = "Big Band"
ID1107 = "Contemporary Jazz"
ID1209 = "Cool Jazz"
ID1108 = "Crossover Jazz"
ID1109 = "Dixieland"
ID1110 = "Fusion"
ID1207 = "Hard Bop"
ID1111 = "Latin Jazz"
ID1112 = "Mainstream Jazz"
ID1113 = "Ragtime"
ID1114 = "Smooth Jazz"
ID1208 = "Trad Jazz"
ID1175 = "Vocal Jazz"
ID52 = "Karaoke"
ID30 = "Kayokyoku"
ID1243 = "Korean"
ID1246 = "Korean Trad Instrumental"
ID1245 = "Korean Trad Song"
ID1247 = "Korean Trad Theater"
ID1244 = "Korean Classical"
ID12 = "Latin"
ID1121 = "Rock y Alternativo"
ID1120 = "Baladas y Boleros"
ID1116 = "Contemporary Latin"
ID1115 = "Latin Jazz"
ID1119 = "Urbano latino"
ID1117 = "Pop Latino"
ID1118 = "Raíces"
ID1123 = "Música Mexicana"
ID1124 = "Música tropical"
ID1291 = "Marching"
ID13 = "New Age"
ID1125 = "Environmental"
ID1126 = "Healing"
ID1127 = "Meditation"
ID1128 = "Nature"
ID1129 = "Relaxation"
ID1130 = "Travel"
ID100021 = "Yoga"
ID1290 = "Orchestral"
ID14 = "Pop"
ID1131 = "Adult Contemporary"
ID1132 = "Britpop"
ID1250 = "C-Pop"
ID1251 = "Cantopop/HK-Pop"
ID100090 = "Egyptian Pop"
ID1259 = "Indo Pop"
ID100089 = "Iraqi Pop"
ID51 = "K-Pop"
ID100092 = "Khaleeji Pop"
ID1252 = "Korean Folk-Pop"
ID100088 = "Levant Pop"
ID100091 = "Maghreb Pop"
ID1255 = "Malaysian Pop"
ID1253 = "Mandopop"
ID1258 = "Manilla Sound"
ID1293 = "Oldies"
ID1257 = "Original Pilipino Music"
ID1256 = "Pinoy Pop"
ID1133 = "Pop/Rock"
ID100076 = "Pop in Russian"
ID100023 = "Shows"
ID1134 = "Soft Rock"
ID1254 = "Tai-Pop"
ID1135 = "Teen Pop"
ID1260 = "Thai Pop"
ID100022 = "Tribute"
ID100069 = "Turkish Pop"
ID15 = "R&B/Soul"
ID1136 = "Contemporary R&B"
ID1137 = "Disco"
ID1138 = "Doo Wop"
ID1139 = "Funk"
ID1140 = "Motown"
ID1141 = "Neo-Soul"
ID1142 = "Quiet Storm"
ID1143 = "Soul"
ID24 = "Reggae"
ID1193 = "Dub"
ID100017 = "Lovers Rock"
ID1183 = "Modern Dancehall"
ID1192 = "Roots Reggae"
ID1194 = "Ska"
ID21 = "Rock"
ID1144 = "Adult Alternative"
ID1145 = "American Trad Rock"
ID1146 = "Arena Rock"
ID1147 = "Blues-Rock"
ID1148 = "British Invasion"
ID1248 = "Chinese Rock"
ID1149 = "Death Metal/Black Metal"
ID1150 = "Glam Rock"
ID1151 = "Hair Metal"
ID1152 = "Hard Rock"
ID1153 = "Metal"
ID1154 = "Jam Bands"
ID1249 = "Korean Rock"
ID1155 = "Prog-Rock/Art Rock"
ID1156 = "Psychedelic"
ID1157 = "Rock & Roll"
ID1158 = "Rockabilly"
ID1159 = "Roots Rock"
ID100077 = "Rock in Russian"
ID1160 = "Singer/Songwriter"
ID1161 = "Southern Rock"
ID1162 = "Surf"
ID1163 = "Tex-Mex"
ID100070 = "Turkish Rock"
ID1299 = "Russian"
ID100075 = "Bard in Russian"
ID1270 = "Chanson in Russian"
ID100074 = "Romance in Russian"
ID10 = "Singer/Songwriter"
ID1062 = "Alternative Folk"
ID1063 = "Contemporary Folk"
ID1064 = "Contemporary Singer/Songwriter"
ID1065 = "Folk-Rock"
ID1066 = "New Acoustic"
ID1067 = "Traditional Folk"
ID16 = "Soundtrack"
ID1165 = "Foreign Cinema"
ID1166 = "Musicals"
ID1168 = "Original Score"
ID1288 = "Sound Effects"
ID1169 = "Soundtrack"
ID1172 = "TV Soundtrack"
ID100032 = "Video Game"
ID50000061 = "Spoken Word"
ID100084 = "Tarab"
ID100086 = "Egyptian Tarab"
ID100085 = "Iraqi Tarab"
ID100087 = "Khaleeji Tarab"
ID1300 = "Turkish"
ID1280 = "Arabesque"
ID100067 = "Fantezi"
ID1272 = "Halk"
ID100068 = "Religious"
ID1273 = "Sanat"
ID100066 = "Özgün"
ID23 = "Vocal"
ID1173 = "Standards"
ID1174 = "Traditional Pop"
ID1261 = "Trot"
ID1176 = "Vocal Pop"
ID19 = "Worldwide"
ID1204 = "Asia"
ID1200 = "Australia"
ID1179 = "Cajun"
ID1276 = "Calypso"
ID1195 = "Caribbean"
ID1180 = "Celtic"
ID1181 = "Celtic Folk"
ID1182 = "Contemporary Celtic"
ID1274 = "Dangdut"
ID1271 = "Dini"
ID1184 = "Drinking Songs"
ID1205 = "Europe"
ID1297 = "Fado"
ID1282 = "Farsi"
ID1295 = "Flamenco"
ID1202 = "France"
ID1199 = "Hawaii"
ID1298 = "Iberia"
ID1275 = "Indonesian Religious"
ID1283 = "Israeli"
ID1201 = "Japan"
ID1186 = "Japanese Pop"
ID1187 = "Klezmer"
ID1198 = "North America"
ID1188 = "Polka"
ID1277 = "Soca"
ID1206 = "South Africa"
ID1196 = "South America"
ID1296 = "Tango"
ID1189 = "Traditional Celtic"
ID1190 = "Worldbeat"
ID1191 = "Zydeco"
+155
View File
@@ -0,0 +1,155 @@
AE = "143481-2,32"
AG = "143540-2,32"
AI = "143538-2,32"
AL = "143575-2,32"
AM = "143524-2,32"
AO = "143564-2,32"
AR = "143505-28,32"
AT = "143445-4,32"
AU = "143460-27,32"
AZ = "143568-2,32"
BB = "143541-2,32"
BE = "143446-2,32"
BF = "143578-2,32"
BG = "143526-2,32"
BH = "143559-2,32"
BJ = "143576-2,32"
BM = "143542-2,32"
BN = "143560-2,32"
BO = "143556-28,32"
BR = "143503-15,32"
BS = "143539-2,32"
BT = "143577-2,32"
BW = "143525-2,32"
BY = "143565-2,32"
BZ = "143555-2,32"
CA = "143455-6,32"
CG = "143582-2,32"
CH = "143459-57,32"
CL = "143483-28,32"
CN = "143465-19,32"
CO = "143501-28,32"
CR = "143495-28,32"
CV = "143580-2,32"
CY = "143557-2,32"
CZ = "143489-2,32"
DE = "143443-4,32"
DK = "143458-2,32"
DM = "143545-2,32"
DO = "143508-28,32"
DZ = "143563-2,32"
EC = "143509-28,32"
EE = "143518-2,32"
EG = "143516-2,32"
ES = "143454-8,32"
FI = "143447-2,32"
FJ = "143583-2,32"
FM = "143591-2,32"
FR = "143442-3,32"
GB = "143444-2,32"
GD = "143546-2,32"
GH = "143573-2,32"
GM = "143584-2,32"
GR = "143448-2,32"
GT = "143504-28,32"
GW = "143585-2,32"
GY = "143553-2,32"
HK = "143463-45,32"
HN = "143510-28,32"
HR = "143494-2,32"
HU = "143482-2,32"
ID = "143476-2,32"
IE = "143449-2,32"
IL = "143491-2,32"
IN = "143467-2,32"
IS = "143558-2,32"
IT = "143450-7,32"
JM = "143511-2,32"
JO = "143528-2,32"
JP = "143462-9,32"
KE = "143529-2,32"
KG = "143586-2,32"
KH = "143579-2,32"
KN = "143548-2,32"
KR = "143466-13,32"
KW = "143493-2,32"
KY = "143544-2,32"
KZ = "143517-2,32"
LA = "143587-2,32"
LB = "143497-2,32"
LC = "143549-2,32"
LK = "143486-2,32"
LR = "143588-2,32"
LT = "143520-2,32"
LU = "143451-2,32"
LV = "143519-2,32"
MD = "143523-2,32"
MG = "143531-2,32"
MK = "143530-2,32"
ML = "143532-2,32"
MN = "143592-2,32"
MO = "143515-45,32"
MR = "143590-2,32"
MS = "143547-2,32"
MT = "143521-2,32"
MU = "143533-2,32"
MW = "143589-2,32"
MX = "143468-28,32"
MY = "143473-2,32"
MZ = "143593-2,32"
NA = "143594-2,32"
NE = "143534-2,32"
NG = "143561-2,32"
NI = "143512-28,32"
NL = "143452-10,32"
NO = "143457-2,32"
NP = "143484-2,32"
NZ = "143461-27,32"
OM = "143562-2,32"
PA = "143485-28,32"
PE = "143507-28,32"
PG = "143597-2,32"
PH = "143474-2,32"
PK = "143477-2,32"
PL = "143478-2,32"
PT = "143453-24,32"
PW = "143595-2,32"
PY = "143513-28,32"
QA = "143498-2,32"
RO = "143487-2,32"
RU = "143469-16,32"
SA = "143479-2,32"
SB = "143601-2,32"
SC = "143599-2,32"
SE = "143456-17,32"
SG = "143464-19,32"
SI = "143499-2,32"
SK = "143496-2,32"
SL = "143600-2,32"
SN = "143535-2,32"
SR = "143554-2,32"
ST = "143598-2,32"
SV = "143506-28,32"
SZ = "143602-2,32"
TC = "143552-2,32"
TD = "143581-2,32"
TH = "143475-2,32"
TJ = "143603-2,32"
TM = "143604-2,32"
TN = "143536-2,32"
TR = "143480-2,32"
TT = "143551-2,32"
TW = "143470-18,32"
TZ = "143572-2,32"
UA = "143492-2,32"
UG = "143537-2,32"
US = "143441-1,32"
UY = "143514-2,32"
UZ = "143566-2,32"
VC = "143550-2,32"
VE = "143502-28,32"
VG = "143543-2,32"
VN = "143471-2,32"
YE = "143571-2,32"
ZA = "143472-2,32"
ZW = "143605-2,32"