diff --git a/.gitignore b/.gitignore index 50534478f..f70b49ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ lbrynet.egg-info __pycache__ _trial_temp/ + +/tests/integration/files diff --git a/lbrynet/extras/daemon/Daemon.py b/lbrynet/extras/daemon/Daemon.py index 7175c0439..9ea530a13 100644 --- a/lbrynet/extras/daemon/Daemon.py +++ b/lbrynet/extras/daemon/Daemon.py @@ -1622,16 +1622,16 @@ class Daemon(metaclass=JSONRPCServerType): @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_channel_create( - self, name, bid, allow_duplicate_name=False, account_id=None, claim_address=None, preview=False, **kwargs): + self, name, bid, allow_duplicate_name=False, account_id=None, claim_address=None, + preview=False, **kwargs): """ Create a new channel by generating a channel private key and establishing an '@' prefixed claim. Usage: channel_create ( | --name=) ( | --bid=) [--allow_duplicate_name=] - [--title=] [--description=<description>] + [--title=<title>] [--description=<description>] [--email=<email>] [--featured=<featured>...] [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...] - [--email=<email>] [--website_url=<website_url>] [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>] [--account_id=<account_id>] [--claim_address=<claim_address>] [--preview] @@ -1642,6 +1642,7 @@ class Daemon(metaclass=JSONRPCServerType): --bid=<bid> : (decimal) amount to back the claim --title=<title> : (str) title of the publication --description=<description> : (str) description of the publication + --featured=<featured> : (list) claim_ids of featured content in channel --tags=<tags> : (list) content tags --languages=<languages> : (list) languages used by the channel, using RFC 5646 format, eg: @@ -1737,6 +1738,7 @@ class Daemon(metaclass=JSONRPCServerType): Usage: channel_update (<claim_id> | --claim_id=<claim_id>) [<bid> | --bid=<bid>] [--title=<title>] [--description=<description>] + [--featured=<featured>...] [--clear_featured] [--tags=<tags>...] [--clear_tags] [--languages=<languages>...] [--clear_languages] [--locations=<locations>...] [--clear_locations] @@ -1749,6 +1751,8 @@ class Daemon(metaclass=JSONRPCServerType): --bid=<bid> : (decimal) amount to back the claim --title=<title> : (str) title of the publication --description=<description> : (str) description of the publication + --clear_featured : (bool) clear existing featured content (prior to adding new ones) + --featured=<featured> : (list) claim_ids of featured content in channel --clear_tags : (bool) clear existing tags (prior to adding new ones) --tags=<tags> : (list) add content tags --clear_languages : (bool) clear existing languages (prior to adding new ones) @@ -1825,9 +1829,10 @@ class Daemon(metaclass=JSONRPCServerType): else: claim_address = old_txo.get_address(account.ledger) - old_txo.claim.channel.update(**kwargs) + claim = Claim.from_bytes(old_txo.claim.to_bytes()) + claim.channel.update(**kwargs) tx = await Transaction.claim_update( - old_txo, amount, claim_address, [account], account + old_txo, claim, amount, claim_address, [account], account ) new_txo = tx.outputs[0] @@ -1970,16 +1975,13 @@ class Daemon(metaclass=JSONRPCServerType): Usage: publish (<name> | --name=<name>) [--bid=<bid>] [--file_path=<file_path>] - [<stream_type> | --stream_type=<stream_type>] [--tags=<tags>...] [--clear_tags] [--languages=<languages>...] [--clear_languages] [--locations=<locations>...] [--clear_locations] [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>] [--title=<title>] [--description=<description>] [--author=<author>] [--language=<language>] [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>] - [--release_time=<release_time>] - [--video_width=<video_width>] [--video_height=<video_height>] [--video_duration=<video_duration>] - [--image_width=<image_width>] [--image_height=<image_height>] [--audio_duration=<audio_duration>] + [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>] [--channel_id=<channel_id>] [--channel_name=<channel_name>] [--channel_account_id=<channel_account_id>...] [--account_id=<account_id>] [--claim_address=<claim_address>] [--preview] @@ -1988,7 +1990,6 @@ class Daemon(metaclass=JSONRPCServerType): --name=<name> : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash)) --bid=<bid> : (decimal) amount to back the claim --file_path=<file_path> : (str) path to file to be associated with name. - --stream_type=<stream_type> : (str) type of stream --fee_currency=<fee_currency> : (string) specify fee currency --fee_amount=<fee_amount> : (decimal) content download fee --fee_address=<fee_address> : (str) address where to send fee payments, will use @@ -2045,17 +2046,10 @@ class Daemon(metaclass=JSONRPCServerType): --license=<license> : (str) publication license --license_url=<license_url> : (str) publication license url --thumbnail_url=<thumbnail_url>: (str) thumbnail url - --release_time=<duration> : (int) original public release of content, seconds since UNIX epoch - --duration=<duration> : (int) audio/video duration in seconds, an attempt will be made to - calculate this automatically if not provided - --image_width=<image_width> : (int) image width - --image_height=<image_height> : (int) image height - --video_width=<video_width> : (int) video width - --video_height=<video_height> : (int) video height - --video_duration=<duration> : (int) video duration in seconds, an attempt will be made to - calculate this automatically if not provided - --audio_duration=<duration> : (int) audio duration in seconds, an attempt will be made to - calculate this automatically if not provided + --release_time=<release_time> : (int) original public release of content, seconds since UNIX epoch + --width=<width> : (int) image/video width, automatically calculated from media file + --height=<height> : (int) image/video height, automatically calculated from media file + --duration=<duration> : (int) audio/video duration in seconds, automatically calculated --channel_id=<channel_id> : (str) claim id of the publisher channel --channel_name=<channel_name> : (str) name of publisher channel --channel_account_id=<channel_id>: (str) one or more account ids for accounts to look in @@ -2094,16 +2088,13 @@ class Daemon(metaclass=JSONRPCServerType): Make a new stream claim and announce the associated file to lbrynet. Usage: - stream_create (<name> | --name=<name>) (<bid> | --bid=<bid>) - (<file_path> | --file_path=<file_path>) [<stream_type> | --stream_type=<stream_type>] + stream_create (<name> | --name=<name>) (<bid> | --bid=<bid>) (<file_path> | --file_path=<file_path>) [--allow_duplicate_name=<allow_duplicate_name>] [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...] [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>] [--title=<title>] [--description=<description>] [--author=<author>] [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>] - [--release_time=<release_time>] - [--video_width=<video_width>] [--video_height=<video_height>] [--video_duration=<video_duration>] - [--image_width=<image_width>] [--image_height=<image_height>] [--audio_duration=<audio_duration>] + [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>] [--channel_id=<channel_id>] [--channel_name=<channel_name>] [--channel_account_id=<channel_account_id>...] [--account_id=<account_id>] [--claim_address=<claim_address>] [--preview] @@ -2114,7 +2105,6 @@ class Daemon(metaclass=JSONRPCServerType): given name. default: false. --bid=<bid> : (decimal) amount to back the claim --file_path=<file_path> : (str) path to file to be associated with name. - --stream_type=<stream_type> : (str) type of stream --fee_currency=<fee_currency> : (string) specify fee currency --fee_amount=<fee_amount> : (decimal) content download fee --fee_address=<fee_address> : (str) address where to send fee payments, will use @@ -2168,17 +2158,10 @@ class Daemon(metaclass=JSONRPCServerType): --license=<license> : (str) publication license --license_url=<license_url> : (str) publication license url --thumbnail_url=<thumbnail_url>: (str) thumbnail url - --release_time=<duration> : (int) original public release of content, seconds since UNIX epoch - --duration=<duration> : (int) audio/video duration in seconds, an attempt will be made to - calculate this automatically if not provided - --image_width=<image_width> : (int) image width - --image_height=<image_height> : (int) image height - --video_width=<video_width> : (int) video width - --video_height=<video_height> : (int) video height - --video_duration=<duration> : (int) video duration in seconds, an attempt will be made to - calculate this automatically if not provided - --audio_duration=<duration> : (int) audio duration in seconds, an attempt will be made to - calculate this automatically if not provided + --release_time=<release_time> : (int) original public release of content, seconds since UNIX epoch + --width=<width> : (int) image/video width, automatically calculated from media file + --height=<height> : (int) image/video height, automatically calculated from media file + --duration=<duration> : (int) audio/video duration in seconds, automatically calculated --channel_id=<channel_id> : (str) claim id of the publisher channel --channel_account_id=<channel_id>: (str) one or more account ids for accounts to look in for channel certificates, defaults to all accounts. @@ -2249,9 +2232,7 @@ class Daemon(metaclass=JSONRPCServerType): [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>] [--title=<title>] [--description=<description>] [--author=<author>] [--language=<language>] [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>] - [--release_time=<release_time>] [--stream_type=<stream_type>] - [--video_width=<video_width>] [--video_height=<video_height>] [--video_duration=<video_duration>] - [--image_width=<image_width>] [--image_height=<image_height>] [--audio_duration=<audio_duration>] + [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>] [--channel_id=<channel_id>] [--channel_name=<channel_name>] [--clear_channel] [--channel_account_id=<channel_account_id>...] [--account_id=<account_id>] [--claim_address=<claim_address>] [--preview] @@ -2316,16 +2297,10 @@ class Daemon(metaclass=JSONRPCServerType): --license=<license> : (str) publication license --license_url=<license_url> : (str) publication license url --thumbnail_url=<thumbnail_url>: (str) thumbnail url - --release_time=<duration> : (int) original public release of content, seconds since UNIX epoch - --stream_type=<stream_type> : (str) type of stream - --image_width=<image_width> : (int) image width - --image_height=<image_height> : (int) image height - --video_width=<video_width> : (int) video width - --video_height=<video_height> : (int) video height - --video_duration=<duration> : (int) video duration in seconds, an attempt will be made to - calculate this automatically if not provided - --audio_duration=<duration> : (int) audio duration in seconds, an attempt will be made to - calculate this automatically if not provided + --release_time=<release_time> : (int) original public release of content, seconds since UNIX epoch + --width=<width> : (int) image/video width, automatically calculated from media file + --height=<height> : (int) image/video height, automatically calculated from media file + --duration=<duration> : (int) audio/video duration in seconds, automatically calculated --channel_id=<channel_id> : (str) claim id of the publisher channel --clear_channel : (bool) remove channel signature --channel_account_id=<channel_id>: (str) one or more account ids for accounts to look in @@ -2366,10 +2341,13 @@ class Daemon(metaclass=JSONRPCServerType): elif old_txo.claim.is_signed and not clear_channel: channel = old_txo.channel - kwargs['fee_address'] = self.get_fee_address(kwargs, claim_address) - old_txo.claim.stream.update(**kwargs) + if 'fee_address' in kwargs: + self.valid_address_or_error(kwargs['fee_address']) + + claim = Claim.from_bytes(old_txo.claim.to_bytes()) + claim.stream.update(file_path=file_path, **kwargs) tx = await Transaction.claim_update( - old_txo, amount, claim_address, [account], account, channel + old_txo, claim, amount, claim_address, [account], account, channel ) new_txo = tx.outputs[0] @@ -3314,7 +3292,8 @@ class Daemon(metaclass=JSONRPCServerType): if 'fee_address' in kwargs: self.valid_address_or_error(kwargs['fee_address']) return kwargs['fee_address'] - return claim_address + if 'fee_currency' in kwargs or 'fee_amount' in kwargs: + return claim_address async def get_receiving_address(self, address: str, account: LBCAccount) -> str: if address is None: diff --git a/lbrynet/extras/daemon/json_response_encoder.py b/lbrynet/extras/daemon/json_response_encoder.py index 35da21c62..b2f62ea32 100644 --- a/lbrynet/extras/daemon/json_response_encoder.py +++ b/lbrynet/extras/daemon/json_response_encoder.py @@ -168,10 +168,7 @@ class JSONResponseEncoder(JSONEncoder): }) if txo.script.is_claim_name or txo.script.is_update_claim: output['value'] = txo.claim - if txo.claim.is_channel: - output['sub_type'] = 'channel' - elif txo.claim.is_stream: - output['sub_type'] = 'stream' + output['sub_type'] = txo.claim.claim_type if txo.channel is not None: output['signing_channel'] = { 'name': txo.channel.claim_name, @@ -210,8 +207,4 @@ class JSONResponseEncoder(JSONEncoder): @staticmethod def encode_claim(claim): - if claim.is_stream: - return claim.stream.to_dict() - elif claim.is_channel: - return claim.channel.to_dict() - return claim.to_dict() + return getattr(claim, claim.claim_type).to_dict() diff --git a/lbrynet/schema/attrs.py b/lbrynet/schema/attrs.py new file mode 100644 index 000000000..dd9e07d03 --- /dev/null +++ b/lbrynet/schema/attrs.py @@ -0,0 +1,512 @@ +import json +import logging +import os.path +import hashlib +from typing import Tuple, List +from string import ascii_letters +from decimal import Decimal, ROUND_UP +from binascii import hexlify, unhexlify + +from torba.client.hash import Base58 +from torba.client.constants import COIN + +from lbrynet.schema.mime_types import guess_media_type +from lbrynet.schema.base import Metadata, BaseMessageList +from lbrynet.schema.types.v2.claim_pb2 import ( + Fee as FeeMessage, + Location as LocationMessage, + Language as LanguageMessage +) + + +log = logging.getLogger(__name__) + + +def calculate_sha256_file_hash(file_path): + sha256 = hashlib.sha256() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(128 * sha256.block_size), b''): + sha256.update(chunk) + return sha256.digest() + + +class Dimmensional(Metadata): + + __slots__ = () + + @property + def width(self) -> int: + return self.message.width + + @width.setter + def width(self, width: int): + self.message.width = width + + @property + def height(self) -> int: + return self.message.height + + @height.setter + def height(self, height: int): + self.message.height = height + + @property + def dimensions(self) -> Tuple[int, int]: + return self.width, self.height + + @dimensions.setter + def dimensions(self, dimensions: Tuple[int, int]): + self.message.width, self.message.height = dimensions + + def _extract(self, file_metadata, field): + try: + setattr(self, field, file_metadata.getValues(field)[0]) + except: + log.exception(f'Could not extract {field} from file metadata.') + + def update(self, file_metadata=None, height=None, width=None): + if height is not None: + self.height = height + elif file_metadata: + self._extract(file_metadata, 'height') + + if width is not None: + self.width = width + elif file_metadata: + self._extract(file_metadata, 'width') + + +class Playable(Metadata): + + __slots__ = () + + @property + def duration(self) -> int: + return self.message.duration + + @duration.setter + def duration(self, duration: int): + self.message.duration = duration + + def update(self, file_metadata=None, duration=None): + if duration is not None: + self.duration = duration + elif file_metadata: + try: + self.duration = file_metadata.getValues('duration')[0].seconds + except: + log.exception('Could not extract duration from file metadata.') + + +class Image(Dimmensional): + + __slots__ = () + + +class Audio(Playable): + + __slots__ = () + + +class Video(Dimmensional, Playable): + + __slots__ = () + + def update(self, file_metadata=None, height=None, width=None, duration=None): + Dimmensional.update(self, file_metadata, height, width) + Playable.update(self, file_metadata, duration) + + +class Source(Metadata): + + __slots__ = () + + def update(self, file_path=None): + if file_path is not None: + self.name = os.path.basename(file_path) + self.media_type, stream_type = guess_media_type(file_path) + if not os.path.isfile(file_path): + raise Exception(f"File does not exist: {file_path}") + self.size = os.path.getsize(file_path) + if self.size == 0: + raise Exception(f"Cannot publish empty file: {file_path}") + self.file_hash_bytes = calculate_sha256_file_hash(file_path) + return stream_type + + @property + def name(self) -> str: + return self.message.name + + @name.setter + def name(self, name: str): + self.message.name = name + + @property + def size(self) -> int: + return self.message.size + + @size.setter + def size(self, size: int): + self.message.size = size + + @property + def media_type(self) -> str: + return self.message.media_type + + @media_type.setter + def media_type(self, media_type: str): + self.message.media_type = media_type + + @property + def file_hash(self) -> str: + return hexlify(self.message.hash).decode() + + @file_hash.setter + def file_hash(self, file_hash: str): + self.message.hash = unhexlify(file_hash.encode()) + + @property + def file_hash_bytes(self) -> bytes: + return self.message.hash + + @file_hash_bytes.setter + def file_hash_bytes(self, file_hash_bytes: bytes): + self.message.hash = file_hash_bytes + + @property + def sd_hash(self) -> str: + return hexlify(self.message.sd_hash).decode() + + @sd_hash.setter + def sd_hash(self, sd_hash: str): + self.message.sd_hash = unhexlify(sd_hash.encode()) + + @property + def sd_hash_bytes(self) -> bytes: + return self.message.sd_hash + + @sd_hash_bytes.setter + def sd_hash_bytes(self, sd_hash: bytes): + self.message.sd_hash = sd_hash + + @property + def url(self) -> str: + return self.message.url + + @url.setter + def url(self, url: str): + self.message.url = url + + +class Fee(Metadata): + + __slots__ = () + + def update(self, address: str = None, currency: str = None, amount=None): + if address is not None: + self.address = address + if currency is not None and amount is not None: + currency = currency.lower() + assert currency in ('lbc', 'btc', 'usd'), f'Unknown currency type: {currency}' + setattr(self, currency, Decimal(amount)) + + @property + def currency(self) -> str: + return FeeMessage.Currency.Name(self.message.currency) + + @property + def address(self) -> str: + return Base58.encode(self.message.address) + + @address.setter + def address(self, address: str): + self.message.address = Base58.decode(address) + + @property + def address_bytes(self) -> bytes: + return self.message.address + + @address_bytes.setter + def address_bytes(self, address: bytes): + self.message.address = address + + @property + def amount(self) -> Decimal: + if self.currency == 'LBC': + return self.lbc + if self.currency == 'BTC': + return self.btc + if self.currency == 'USD': + return self.usd + + DEWIES = Decimal(COIN) + + @property + def lbc(self) -> Decimal: + if self.message.currency != FeeMessage.LBC: + raise ValueError('LBC can only be returned for LBC fees.') + return Decimal(self.message.amount / self.DEWIES) + + @lbc.setter + def lbc(self, amount: Decimal): + self.dewies = int(amount * self.DEWIES) + + @property + def dewies(self) -> int: + if self.message.currency != FeeMessage.LBC: + raise ValueError('Dewies can only be returned for LBC fees.') + return self.message.amount + + @dewies.setter + def dewies(self, amount: int): + self.message.amount = amount + self.message.currency = FeeMessage.LBC + + SATOSHIES = Decimal(COIN) + + @property + def btc(self) -> Decimal: + if self.message.currency != FeeMessage.BTC: + raise ValueError('BTC can only be returned for BTC fees.') + return Decimal(self.message.amount / self.SATOSHIES) + + @btc.setter + def btc(self, amount: Decimal): + self.satoshis = int(amount * self.SATOSHIES) + + @property + def satoshis(self) -> int: + if self.message.currency != FeeMessage.BTC: + raise ValueError('Satoshies can only be returned for BTC fees.') + return self.message.amount + + @satoshis.setter + def satoshis(self, amount: int): + self.message.amount = amount + self.message.currency = FeeMessage.BTC + + PENNIES = Decimal('100.0') + PENNY = Decimal('0.01') + + @property + def usd(self) -> Decimal: + if self.message.currency != FeeMessage.USD: + raise ValueError('USD can only be returned for USD fees.') + return Decimal(self.message.amount / self.PENNIES) + + @usd.setter + def usd(self, amount: Decimal): + self.pennies = int(amount.quantize(self.PENNY, ROUND_UP) * self.PENNIES) + + @property + def pennies(self) -> int: + if self.message.currency != FeeMessage.USD: + raise ValueError('Pennies can only be returned for USD fees.') + return self.message.amount + + @pennies.setter + def pennies(self, amount: int): + self.message.amount = amount + self.message.currency = FeeMessage.USD + + +class ClaimReference(Metadata): + + __slots__ = () + + @property + def claim_id(self) -> str: + return hexlify(self.claim_hash[::-1]).decode() + + @claim_id.setter + def claim_id(self, claim_id: str): + self.claim_hash = unhexlify(claim_id)[::-1] + + @property + def claim_hash(self) -> bytes: + return self.message.claim_hash + + @claim_hash.setter + def claim_hash(self, claim_hash: bytes): + self.message.claim_hash = claim_hash + + +class ClaimList(BaseMessageList[ClaimReference]): + + __slots__ = () + item_class = ClaimReference + + @property + def _message(self): + return self.message.claim_references + + def append(self, value): + self.add().claim_id = value + + @property + def ids(self) -> List[str]: + return [c.claim_id for c in self] + + +class Language(Metadata): + + __slots__ = () + + @property + def langtag(self) -> str: + langtag = [] + if self.language: + langtag.append(self.language) + if self.script: + langtag.append(self.script) + if self.region: + langtag.append(self.region) + return '-'.join(langtag) + + @langtag.setter + def langtag(self, langtag: str): + parts = langtag.split('-') + self.language = parts.pop(0) + if parts and len(parts[0]) == 4: + self.script = parts.pop(0) + if parts and len(parts[0]) == 2: + self.region = parts.pop(0) + assert not parts, f"Failed to parse language tag: {langtag}" + + @property + def language(self) -> str: + if self.message.language: + return LanguageMessage.Language.Name(self.message.language) + + @language.setter + def language(self, language: str): + self.message.language = LanguageMessage.Language.Value(language) + + @property + def script(self) -> str: + if self.message.script: + return LanguageMessage.Script.Name(self.message.script) + + @script.setter + def script(self, script: str): + self.message.script = LanguageMessage.Script.Value(script) + + @property + def region(self) -> str: + if self.message.region: + return LocationMessage.Country.Name(self.message.region) + + @region.setter + def region(self, region: str): + self.message.region = LocationMessage.Country.Value(region) + + +class LanguageList(BaseMessageList[Language]): + __slots__ = () + item_class = Language + + def append(self, value: str): + self.add().langtag = value + + +class Location(Metadata): + + __slots__ = () + + def from_value(self, value): + if isinstance(value, str) and value.startswith('{'): + value = json.loads(value) + + if isinstance(value, dict): + for key, val in value.items(): + setattr(self, key, val) + + elif isinstance(value, str): + parts = value.split(':') + if len(parts) > 2 or (parts[0] and parts[0][0] in ascii_letters): + country = parts and parts.pop(0) + if country: + self.country = country + state = parts and parts.pop(0) + if state: + self.state = state + city = parts and parts.pop(0) + if city: + self.city = city + code = parts and parts.pop(0) + if code: + self.code = code + latitude = parts and parts.pop(0) + if latitude: + self.latitude = latitude + longitude = parts and parts.pop(0) + if longitude: + self.longitude = longitude + + else: + raise ValueError(f'Could not parse country value: {value}') + + @property + def country(self) -> str: + if self.message.country: + return LocationMessage.Country.Name(self.message.country) + + @country.setter + def country(self, country: str): + self.message.country = LocationMessage.Country.Value(country) + + @property + def state(self) -> str: + return self.message.state + + @state.setter + def state(self, state: str): + self.message.state = state + + @property + def city(self) -> str: + return self.message.city + + @city.setter + def city(self, city: str): + self.message.city = city + + @property + def code(self) -> str: + return self.message.code + + @code.setter + def code(self, code: str): + self.message.code = code + + GPS_PRECISION = Decimal('10000000') + + @property + def latitude(self) -> str: + if self.message.latitude: + return str(Decimal(self.message.latitude) / self.GPS_PRECISION) + + @latitude.setter + def latitude(self, latitude: str): + latitude = Decimal(latitude) + assert -90 <= latitude <= 90, "Latitude must be between -90 and 90 degrees." + self.message.latitude = int(latitude * self.GPS_PRECISION) + + @property + def longitude(self) -> str: + if self.message.longitude: + return str(Decimal(self.message.longitude) / self.GPS_PRECISION) + + @longitude.setter + def longitude(self, longitude: str): + longitude = Decimal(longitude) + assert -180 <= longitude <= 180, "Longitude must be between -180 and 180 degrees." + self.message.longitude = int(longitude * self.GPS_PRECISION) + + +class LocationList(BaseMessageList[Location]): + __slots__ = () + item_class = Location + + def append(self, value): + self.add().from_value(value) diff --git a/lbrynet/schema/base.py b/lbrynet/schema/base.py index d0516e57c..343fd1736 100644 --- a/lbrynet/schema/base.py +++ b/lbrynet/schema/base.py @@ -1,4 +1,5 @@ from binascii import hexlify, unhexlify +from typing import List, Iterator, TypeVar, Generic from google.protobuf.message import DecodeError from google.protobuf.json_format import MessageToDict @@ -21,9 +22,10 @@ class Signable: self.unsigned_payload = None self.signing_channel_hash = None - @property - def is_undetermined(self): - return self.message.WhichOneof('type') is None + def clear_signature(self): + self.signature = None + self.unsigned_payload = None + self.signing_channel_hash = None @property def signing_channel_id(self): @@ -72,3 +74,48 @@ class Signable: def __bytes__(self): return self.to_bytes() + + +class Metadata: + + __slots__ = 'message', + + def __init__(self, message): + self.message = message + + +I = TypeVar('I') + + +class BaseMessageList(Metadata, Generic[I]): + + __slots__ = () + + item_class = None + + @property + def _message(self): + return self.message + + def add(self) -> I: + return self.item_class(self._message.add()) + + def extend(self, values: List[str]): + for value in values: + self.append(value) + + def append(self, value: str): + raise NotImplemented + + def __len__(self): + return len(self._message) + + def __iter__(self) -> Iterator[I]: + for item in self._message: + yield self.item_class(item) + + def __getitem__(self, item) -> I: + return self.item_class(self._message[item]) + + def __delitem__(self, key): + del self._message[key] diff --git a/lbrynet/schema/claim.py b/lbrynet/schema/claim.py index bb15d15fe..7307a3322 100644 --- a/lbrynet/schema/claim.py +++ b/lbrynet/schema/claim.py @@ -1,76 +1,85 @@ -import os.path -import json -from string import ascii_letters -from typing import List, Tuple, Iterator, TypeVar, Generic -from decimal import Decimal, ROUND_UP +import logging +from typing import List from binascii import hexlify, unhexlify from google.protobuf.json_format import MessageToDict from google.protobuf.message import DecodeError +from hachoir.core.log import log as hachoir_log from hachoir.parser import createParser as binary_file_parser from hachoir.metadata import extractMetadata as binary_file_metadata -from hachoir.core.log import log as hachoir_log - -from torba.client.hash import Base58 -from torba.client.constants import COIN from lbrynet.schema import compat from lbrynet.schema.base import Signable -from lbrynet.schema.mime_types import guess_media_type -from lbrynet.schema.types.v2.claim_pb2 import ( - Claim as ClaimMessage, - Fee as FeeMessage, - Location as LocationMessage, - Language as LanguageMessage +from lbrynet.schema.mime_types import guess_media_type, guess_stream_type +from lbrynet.schema.attrs import ( + Source, Playable, Dimmensional, Fee, Image, Video, Audio, + LanguageList, LocationList, ClaimList, ClaimReference ) +from lbrynet.schema.types.v2.claim_pb2 import Claim as ClaimMessage hachoir_log.use_print = False +log = logging.getLogger(__name__) class Claim(Signable): + STREAM = 'stream' + CHANNEL = 'channel' + COLLECTION = 'collection' + REPOST = 'repost' + __slots__ = 'version', + message_class = ClaimMessage - def __init__(self, claim_message=None): - super().__init__(claim_message) + def __init__(self, message=None): + super().__init__(message) self.version = 2 + @property + def claim_type(self) -> str: + return self.message.WhichOneof('type') + + def get_message(self, type_name): + message = getattr(self.message, type_name) + if self.claim_type is None: + message.SetInParent() + if self.claim_type != type_name: + raise ValueError(f'Claim is not a {type_name}.') + return message + @property def is_stream(self): - return self.message.WhichOneof('type') == 'stream' - - @property - def is_channel(self): - return self.message.WhichOneof('type') == 'channel' - - @property - def stream_message(self): - if self.is_undetermined: - self.message.stream.SetInParent() - if not self.is_stream: - raise ValueError('Claim is not a stream.') - return self.message.stream + return self.claim_type == self.STREAM @property def stream(self) -> 'Stream': return Stream(self) @property - def channel_message(self): - if self.is_undetermined: - self.message.channel.SetInParent() - if not self.is_channel: - raise ValueError('Claim is not a channel.') - return self.message.channel + def is_channel(self): + return self.claim_type == self.CHANNEL @property def channel(self) -> 'Channel': return Channel(self) - def to_dict(self): - return MessageToDict(self.message, preserving_proto_field_name=True) + @property + def is_repost(self): + return self.claim_type == self.REPOST + + @property + def repost(self) -> 'Repost': + return Repost(self) + + @property + def is_collection(self): + return self.claim_type == self.COLLECTION + + @property + def collection(self) -> 'Collection': + return Collection(self) @classmethod def from_bytes(cls, data: bytes) -> 'Claim': @@ -89,494 +98,21 @@ class Claim(Signable): return claim -I = TypeVar('I') - - -class BaseMessageList(Generic[I]): - - __slots__ = 'message', - - item_class = None - - def __init__(self, message): - self.message = message - - def add(self) -> I: - return self.item_class(self.message.add()) - - def extend(self, values: List[str]): - for value in values: - self.append(value) - - def append(self, value: str): - raise NotImplemented - - def __len__(self): - return len(self.message) - - def __iter__(self) -> Iterator[I]: - for lang in self.message: - yield self.item_class(lang) - - def __getitem__(self, item) -> I: - return self.item_class(self.message[item]) - - -class Dimmensional: - - __slots__ = () - - @property - def width(self) -> int: - return self.message.width - - @width.setter - def width(self, width: int): - self.message.width = width - - @property - def height(self) -> int: - return self.message.height - - @height.setter - def height(self, height: int): - self.message.height = height - - @property - def dimensions(self) -> Tuple[int, int]: - return self.width, self.height - - @dimensions.setter - def dimensions(self, dimensions: Tuple[int, int]): - self.message.width, self.message.height = dimensions - - -class Playable: - - __slots__ = () - - @property - def duration(self) -> int: - return self.message.duration - - @duration.setter - def duration(self, duration: int): - self.message.duration = duration - - def set_duration_from_path(self, file_path): - try: - file_metadata = binary_file_metadata(binary_file_parser(file_path)) - self.duration = file_metadata.getValues('duration')[0].seconds - except: - pass - - -class Image(Dimmensional): - - __slots__ = 'message', - - def __init__(self, image_message): - self.message = image_message - - -class Video(Dimmensional, Playable): - - __slots__ = 'message', - - def __init__(self, video_message): - self.message = video_message - - -class Audio(Playable): - - __slots__ = 'message', - - def __init__(self, audio_message): - self.message = audio_message - - -class Source: - - __slots__ = 'message', - - def __init__(self, file_message): - self.message = file_message - - @property - def name(self) -> str: - return self.message.name - - @name.setter - def name(self, name: str): - self.message.name = name - - @property - def size(self) -> int: - return self.message.size - - @size.setter - def size(self, size: int): - self.message.size = size - - @property - def media_type(self) -> str: - return self.message.media_type - - @media_type.setter - def media_type(self, media_type: str): - self.message.media_type = media_type - - @property - def sd_hash(self) -> str: - return hexlify(self.message.sd_hash).decode() - - @sd_hash.setter - def sd_hash(self, sd_hash: str): - self.message.sd_hash = unhexlify(sd_hash.encode()) - - @property - def sd_hash_bytes(self) -> bytes: - return self.message.sd_hash - - @sd_hash_bytes.setter - def sd_hash_bytes(self, sd_hash: bytes): - self.message.sd_hash = sd_hash - - @property - def url(self) -> str: - return self.message.url - - @url.setter - def url(self, url: str): - self.message.url = url - - -class Fee: - - __slots__ = '_fee', - - def __init__(self, fee_message): - self._fee = fee_message - - @property - def currency(self) -> str: - return FeeMessage.Currency.Name(self._fee.currency) - - @property - def address(self) -> str: - return Base58.encode(self._fee.address) - - @address.setter - def address(self, address: str): - self._fee.address = Base58.decode(address) - - @property - def address_bytes(self) -> bytes: - return self._fee.address - - @address_bytes.setter - def address_bytes(self, address: bytes): - self._fee.address = address - - @property - def amount(self) -> Decimal: - if self.currency == 'LBC': - return self.lbc - if self.currency == 'BTC': - return self.btc - if self.currency == 'USD': - return self.usd - - DEWIES = Decimal(COIN) - - @property - def lbc(self) -> Decimal: - if self._fee.currency != FeeMessage.LBC: - raise ValueError('LBC can only be returned for LBC fees.') - return Decimal(self._fee.amount / self.DEWIES) - - @lbc.setter - def lbc(self, amount: Decimal): - self.dewies = int(amount * self.DEWIES) - - @property - def dewies(self) -> int: - if self._fee.currency != FeeMessage.LBC: - raise ValueError('Dewies can only be returned for LBC fees.') - return self._fee.amount - - @dewies.setter - def dewies(self, amount: int): - self._fee.amount = amount - self._fee.currency = FeeMessage.LBC - - SATOSHIES = Decimal(COIN) - - @property - def btc(self) -> Decimal: - if self._fee.currency != FeeMessage.BTC: - raise ValueError('BTC can only be returned for BTC fees.') - return Decimal(self._fee.amount / self.SATOSHIES) - - @btc.setter - def btc(self, amount: Decimal): - self.satoshis = int(amount * self.SATOSHIES) - - @property - def satoshis(self) -> int: - if self._fee.currency != FeeMessage.BTC: - raise ValueError('Satoshies can only be returned for BTC fees.') - return self._fee.amount - - @satoshis.setter - def satoshis(self, amount: int): - self._fee.amount = amount - self._fee.currency = FeeMessage.BTC - - PENNIES = Decimal('100.0') - PENNY = Decimal('0.01') - - @property - def usd(self) -> Decimal: - if self._fee.currency != FeeMessage.USD: - raise ValueError('USD can only be returned for USD fees.') - return Decimal(self._fee.amount / self.PENNIES) - - @usd.setter - def usd(self, amount: Decimal): - self.pennies = int(amount.quantize(self.PENNY, ROUND_UP) * self.PENNIES) - - @property - def pennies(self) -> int: - if self._fee.currency != FeeMessage.USD: - raise ValueError('Pennies can only be returned for USD fees.') - return self._fee.amount - - @pennies.setter - def pennies(self, amount: int): - self._fee.amount = amount - self._fee.currency = FeeMessage.USD - - -class ClaimReference: - - __slots__ = 'message', - - def __init__(self, message): - self.message = message - - @property - def claim_id(self) -> str: - return hexlify(self.claim_hash[::-1]).decode() - - @claim_id.setter - def claim_id(self, claim_id: str): - self.claim_hash = unhexlify(claim_id)[::-1] - - @property - def claim_hash(self) -> bytes: - return self.message.claim_hash - - @claim_hash.setter - def claim_hash(self, claim_hash: bytes): - self.message.claim_hash = claim_hash - - -class ClaimList(BaseMessageList[ClaimReference]): - - __slots__ = () - item_class = ClaimReference - - def append(self, value): - self.add().claim_id = value - - @property - def claim_ids(self) -> List[str]: - return [c.claim_id for c in self] - - -class Language: - - __slots__ = 'message', - - def __init__(self, message): - self.message = message - - @property - def langtag(self) -> str: - langtag = [] - if self.language: - langtag.append(self.language) - if self.script: - langtag.append(self.script) - if self.region: - langtag.append(self.region) - return '-'.join(langtag) - - @langtag.setter - def langtag(self, langtag: str): - parts = langtag.split('-') - self.language = parts.pop(0) - if parts and len(parts[0]) == 4: - self.script = parts.pop(0) - if parts and len(parts[0]) == 2: - self.region = parts.pop(0) - assert not parts, f"Failed to parse language tag: {langtag}" - - @property - def language(self) -> str: - if self.message.language: - return LanguageMessage.Language.Name(self.message.language) - - @language.setter - def language(self, language: str): - self.message.language = LanguageMessage.Language.Value(language) - - @property - def script(self) -> str: - if self.message.script: - return LanguageMessage.Script.Name(self.message.script) - - @script.setter - def script(self, script: str): - self.message.script = LanguageMessage.Script.Value(script) - - @property - def region(self) -> str: - if self.message.region: - return LocationMessage.Country.Name(self.message.region) - - @region.setter - def region(self, region: str): - self.message.region = LocationMessage.Country.Value(region) - - -class LanguageList(BaseMessageList[Language]): - __slots__ = () - item_class = Language - - def append(self, value: str): - self.add().langtag = value - - -class Location: - - __slots__ = 'message', - - def __init__(self, message): - self.message = message - - def from_value(self, value): - if isinstance(value, str) and value.startswith('{'): - value = json.loads(value) - - if isinstance(value, dict): - for key, val in value.items(): - setattr(self, key, val) - - elif isinstance(value, str): - parts = value.split(':') - if len(parts) > 2 or (parts[0] and parts[0][0] in ascii_letters): - country = parts and parts.pop(0) - if country: - self.country = country - state = parts and parts.pop(0) - if state: - self.state = state - city = parts and parts.pop(0) - if city: - self.city = city - code = parts and parts.pop(0) - if code: - self.code = code - latitude = parts and parts.pop(0) - if latitude: - self.latitude = latitude - longitude = parts and parts.pop(0) - if longitude: - self.longitude = longitude - - else: - raise ValueError(f'Could not parse country value: {value}') - - @property - def country(self) -> str: - if self.message.country: - return LocationMessage.Country.Name(self.message.country) - - @country.setter - def country(self, country: str): - self.message.country = LocationMessage.Country.Value(country) - - @property - def state(self) -> str: - return self.message.state - - @state.setter - def state(self, state: str): - self.message.state = state - - @property - def city(self) -> str: - return self.message.city - - @city.setter - def city(self, city: str): - self.message.city = city - - @property - def code(self) -> str: - return self.message.code - - @code.setter - def code(self, code: str): - self.message.code = code - - GPS_PRECISION = Decimal('10000000') - - @property - def latitude(self) -> str: - if self.message.latitude: - return str(Decimal(self.message.latitude) / self.GPS_PRECISION) - - @latitude.setter - def latitude(self, latitude: str): - latitude = Decimal(latitude) - assert -90 <= latitude <= 90, "Latitude must be between -90 and 90 degrees." - self.message.latitude = int(latitude * self.GPS_PRECISION) - - @property - def longitude(self) -> str: - if self.message.longitude: - return str(Decimal(self.message.longitude) / self.GPS_PRECISION) - - @longitude.setter - def longitude(self, longitude: str): - longitude = Decimal(longitude) - assert -180 <= longitude <= 180, "Longitude must be between -180 and 180 degrees." - self.message.longitude = int(longitude * self.GPS_PRECISION) - - -class LocationList(BaseMessageList[Location]): - __slots__ = () - item_class = Location - - def append(self, value): - self.add().from_value(value) - - -class BaseClaimSubType: +class BaseClaim: __slots__ = 'claim', 'message' + claim_type = None object_fields = 'thumbnail', repeat_fields = 'tags', 'languages', 'locations' - def __init__(self, claim: Claim): + def __init__(self, claim: Claim = None): self.claim = claim or Claim() + self.message = self.claim.get_message(self.claim_type) def to_dict(self): - claim = self.claim.to_dict() + claim = MessageToDict(self.claim.message, preserving_proto_field_name=True) + claim.update(claim.pop(self.claim_type)) if 'languages' in claim: claim['languages'] = self.langtags return claim @@ -642,78 +178,19 @@ class BaseClaimSubType: return LocationList(self.claim.message.locations) -class Channel(BaseClaimSubType): +class Stream(BaseClaim): __slots__ = () - object_fields = BaseClaimSubType.object_fields + ('cover',) - repeat_fields = BaseClaimSubType.repeat_fields + ('featured',) + claim_type = Claim.STREAM - def __init__(self, claim: Claim = None): - super().__init__(claim) - self.message = self.claim.channel_message + object_fields = BaseClaim.object_fields + ('source',) def to_dict(self): claim = super().to_dict() - claim.update(claim.pop('channel')) - claim['public_key'] = self.public_key - return claim - - @property - def public_key(self) -> str: - return hexlify(self.message.public_key).decode() - - @public_key.setter - def public_key(self, sd_public_key: str): - self.message.public_key = unhexlify(sd_public_key.encode()) - - @property - def public_key_bytes(self) -> bytes: - return self.message.public_key - - @public_key_bytes.setter - def public_key_bytes(self, public_key: bytes): - self.message.public_key = public_key - - @property - def email(self) -> str: - return self.message.email - - @email.setter - def email(self, email: str): - self.message.email = email - - @property - def website_url(self) -> str: - return self.message.website_url - - @website_url.setter - def website_url(self, website_url: str): - self.message.website_url = website_url - - @property - def cover(self) -> Source: - return Source(self.message.cover) - - @property - def featured(self) -> ClaimList: - return ClaimList(self.message.featured) - - -class Stream(BaseClaimSubType): - - __slots__ = () - - object_fields = BaseClaimSubType.object_fields + ('source',) - - def __init__(self, claim: Claim = None): - super().__init__(claim) - self.message = self.claim.stream_message - - def to_dict(self): - claim = super().to_dict() - claim.update(claim.pop('stream')) if 'source' in claim: + if 'hash' in claim['source']: + claim['source']['hash'] = self.source.file_hash if 'sd_hash' in claim['source']: claim['source']['sd_hash'] = self.source.sd_hash fee = claim.get('fee', {}) @@ -723,58 +200,39 @@ class Stream(BaseClaimSubType): fee['amount'] = self.fee.amount return claim - def update( - self, file_path=None, stream_type=None, - fee_currency=None, fee_amount=None, fee_address=None, - **kwargs): - - duration_was_not_set = True - sub_types = ('image', 'video', 'audio') - for key in list(kwargs.keys()): - for sub_type in sub_types: - if key.startswith(f'{sub_type}_'): - stream_type = sub_type - sub_obj = getattr(self, sub_type) - sub_obj_attr = key[len(f'{sub_type}_'):] - setattr(sub_obj, sub_obj_attr, kwargs.pop(key)) - if sub_obj_attr == 'duration': - duration_was_not_set = False - break - - if stream_type is not None: - if stream_type not in sub_types: - raise Exception( - f"stream_type of '{stream_type}' is not valid, must be one of: {sub_types}" - ) - - sub_obj = getattr(self, stream_type) - if duration_was_not_set and file_path and isinstance(sub_obj, Playable): - sub_obj.set_duration_from_path(file_path) + def update(self, file_path=None, height=None, width=None, duration=None, **kwargs): + self.fee.update( + kwargs.pop('fee_address', None), + kwargs.pop('fee_currency', None), + kwargs.pop('fee_amount', None) + ) if 'sd_hash' in kwargs: self.source.sd_hash = kwargs.pop('sd_hash') - super().update(**kwargs) - + stream_type = None if file_path is not None: - self.source.media_type = guess_media_type(file_path) - if not os.path.isfile(file_path): - raise Exception(f"File does not exist: {file_path}") - self.source.size = os.path.getsize(file_path) - if self.source.size == 0: - raise Exception(f"Cannot publish empty file: {file_path}") + stream_type = self.source.update(file_path=file_path) + elif self.source.name: + self.source.media_type, stream_type = guess_media_type(self.source.name) + elif self.source.media_type: + stream_type = guess_stream_type(self.source.media_type) - if fee_amount and fee_currency: - if fee_address: - self.fee.address = fee_address - if fee_currency.lower() == 'lbc': - self.fee.lbc = Decimal(fee_amount) - elif fee_currency.lower() == 'btc': - self.fee.btc = Decimal(fee_amount) - elif fee_currency.lower() == 'usd': - self.fee.usd = Decimal(fee_amount) - else: - raise Exception(f'Unknown currency type: {fee_currency}') + if stream_type in ('image', 'video', 'audio'): + media = getattr(self, stream_type) + media_args = {'file_metadata': None} + try: + media_args['file_metadata'] = binary_file_metadata(binary_file_parser(file_path)) + except: + log.exception('Could not read file metadata.') + if isinstance(media, Playable): + media_args['duration'] = duration + if isinstance(media, Dimmensional): + media_args['height'] = height + media_args['width'] = width + media.update(**media_args) + + super().update(**kwargs) @property def author(self) -> str: @@ -831,3 +289,90 @@ class Stream(BaseClaimSubType): @property def audio(self) -> Audio: return Audio(self.message.audio) + + +class Channel(BaseClaim): + + __slots__ = () + + claim_type = Claim.CHANNEL + + object_fields = BaseClaim.object_fields + ('cover',) + repeat_fields = BaseClaim.repeat_fields + ('featured',) + + def to_dict(self): + claim = super().to_dict() + claim['public_key'] = self.public_key + if 'featured' in claim: + claim['featured'] = self.featured.ids + return claim + + @property + def public_key(self) -> str: + return hexlify(self.message.public_key).decode() + + @public_key.setter + def public_key(self, sd_public_key: str): + self.message.public_key = unhexlify(sd_public_key.encode()) + + @property + def public_key_bytes(self) -> bytes: + return self.message.public_key + + @public_key_bytes.setter + def public_key_bytes(self, public_key: bytes): + self.message.public_key = public_key + + @property + def email(self) -> str: + return self.message.email + + @email.setter + def email(self, email: str): + self.message.email = email + + @property + def website_url(self) -> str: + return self.message.website_url + + @website_url.setter + def website_url(self, website_url: str): + self.message.website_url = website_url + + @property + def cover(self) -> Source: + return Source(self.message.cover) + + @property + def featured(self) -> ClaimList: + return ClaimList(self.message.featured) + + +class Repost(BaseClaim): + + __slots__ = () + + claim_type = Claim.REPOST + + @property + def reference(self) -> ClaimReference: + return ClaimReference(self.message) + + +class Collection(BaseClaim): + + __slots__ = () + + claim_type = Claim.COLLECTION + + repeat_fields = BaseClaim.repeat_fields + ('claims',) + + def to_dict(self): + claim = super().to_dict() + if 'claim_references' in claim: + claim['claim_references'] = self.claims.ids + return claim + + @property + def claims(self) -> ClaimList: + return ClaimList(self.message) diff --git a/lbrynet/schema/mime_types.py b/lbrynet/schema/mime_types.py index 201219233..8ac333d6a 100644 --- a/lbrynet/schema/mime_types.py +++ b/lbrynet/schema/mime_types.py @@ -162,6 +162,13 @@ def guess_media_type(path): extension = ext.strip().lower() if extension[1:]: if extension in types_map: - return types_map[extension][0] - return f'application/x-ext-{extension[1:]}' - return 'application/octet-stream' + return types_map[extension] + return f'application/x-ext-{extension[1:]}', 'binary' + return 'application/octet-stream', 'binary' + + +def guess_stream_type(media_type): + for media, stream in types_map.values(): + if media == media_type: + return stream + return 'binary' diff --git a/lbrynet/schema/types/v2/claim_pb2.py b/lbrynet/schema/types/v2/claim_pb2.py index a1e2ac127..306d1ad0e 100644 --- a/lbrynet/schema/types/v2/claim_pb2.py +++ b/lbrynet/schema/types/v2/claim_pb2.py @@ -19,7 +19,7 @@ DESCRIPTOR = _descriptor.FileDescriptor( package='pb', syntax='proto3', serialized_options=None, - serialized_pb=_b('\n\x0b\x63laim.proto\x12\x02pb\"\xab\x02\n\x05\x43laim\x12\x1c\n\x06stream\x18\x01 \x01(\x0b\x32\n.pb.StreamH\x00\x12\x1e\n\x07\x63hannel\x18\x02 \x01(\x0b\x32\x0b.pb.ChannelH\x00\x12#\n\nclaim_list\x18\x03 \x01(\x0b\x32\r.pb.ClaimListH\x00\x12$\n\x06repost\x18\x04 \x01(\x0b\x32\x12.pb.ClaimReferenceH\x00\x12\r\n\x05title\x18\x08 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\t \x01(\t\x12\x1d\n\tthumbnail\x18\n \x01(\x0b\x32\n.pb.Source\x12\x0c\n\x04tags\x18\x0b \x03(\t\x12\x1f\n\tlanguages\x18\x0c \x03(\x0b\x32\x0c.pb.Language\x12\x1f\n\tlocations\x18\r \x03(\x0b\x32\x0c.pb.LocationB\x06\n\x04type\"\x84\x02\n\x06Stream\x12\x1a\n\x06source\x18\x01 \x01(\x0b\x32\n.pb.Source\x12\x0e\n\x06\x61uthor\x18\x02 \x01(\t\x12\x0f\n\x07license\x18\x03 \x01(\t\x12\x13\n\x0blicense_url\x18\x04 \x01(\t\x12\x14\n\x0crelease_time\x18\x05 \x01(\x03\x12\x14\n\x03\x66\x65\x65\x18\x06 \x01(\x0b\x32\x07.pb.Fee\x12\x1a\n\x05image\x18\n \x01(\x0b\x32\t.pb.ImageH\x00\x12\x1a\n\x05video\x18\x0b \x01(\x0b\x32\t.pb.VideoH\x00\x12\x1a\n\x05\x61udio\x18\x0c \x01(\x0b\x32\t.pb.AudioH\x00\x12 \n\x08software\x18\r \x01(\x0b\x32\x0c.pb.SoftwareH\x00\x42\x06\n\x04type\"}\n\x07\x43hannel\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x13\n\x0bwebsite_url\x18\x03 \x01(\t\x12\x19\n\x05\x63over\x18\x04 \x01(\x0b\x32\n.pb.Source\x12\x1f\n\x08\x66\x65\x61tured\x18\x05 \x01(\x0b\x32\r.pb.ClaimList\"$\n\x0e\x43laimReference\x12\x12\n\nclaim_hash\x18\x01 \x01(\x0c\"\xa7\x01\n\tClaimList\x12)\n\tlist_type\x18\x01 \x01(\x0e\x32\x16.pb.ClaimList.ListType\x12,\n\x10\x63laim_references\x18\x02 \x03(\x0b\x32\x12.pb.ClaimReference\"A\n\x08ListType\x12\x15\n\x11UNKNOWN_LIST_TYPE\x10\x00\x12\x0e\n\nCOLLECTION\x10\x01\x12\x0e\n\nDERIVATION\x10\x02\"d\n\x06Source\x12\x0c\n\x04hash\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x04\x12\x12\n\nmedia_type\x18\x04 \x01(\t\x12\x0b\n\x03url\x18\x05 \x01(\t\x12\x0f\n\x07sd_hash\x18\x06 \x01(\x0c\"\x87\x01\n\x03\x46\x65\x65\x12\"\n\x08\x63urrency\x18\x01 \x01(\x0e\x32\x10.pb.Fee.Currency\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\";\n\x08\x43urrency\x12\x14\n\x10UNKNOWN_CURRENCY\x10\x00\x12\x07\n\x03LBC\x10\x01\x12\x07\n\x03\x42TC\x10\x02\x12\x07\n\x03USD\x10\x03\"&\n\x05Image\x12\r\n\x05width\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\r\"@\n\x05Video\x12\r\n\x05width\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\r\x12\x18\n\x05\x61udio\x18\x03 \x01(\x0b\x32\t.pb.Audio\"\x19\n\x05\x41udio\x12\x10\n\x08\x64uration\x18\x01 \x01(\r\"l\n\x08Software\x12\n\n\x02os\x18\x01 \x01(\t\"T\n\x02OS\x12\x0e\n\nUNKNOWN_OS\x10\x00\x12\x07\n\x03\x41NY\x10\x01\x12\t\n\x05LINUX\x10\x02\x12\x0b\n\x07WINDOWS\x10\x03\x12\x07\n\x03MAC\x10\x04\x12\x0b\n\x07\x41NDROID\x10\x05\x12\x07\n\x03IOS\x10\x06\"\xc7\x1d\n\x08Language\x12\'\n\x08language\x18\x01 \x01(\x0e\x32\x15.pb.Language.Language\x12#\n\x06script\x18\x02 \x01(\x0e\x32\x13.pb.Language.Script\x12$\n\x06region\x18\x03 \x01(\x0e\x32\x14.pb.Location.Country\"\x99\x0c\n\x08Language\x12\x14\n\x10UNKNOWN_LANGUAGE\x10\x00\x12\x06\n\x02\x65n\x10\x01\x12\x06\n\x02\x61\x61\x10\x02\x12\x06\n\x02\x61\x62\x10\x03\x12\x06\n\x02\x61\x65\x10\x04\x12\x06\n\x02\x61\x66\x10\x05\x12\x06\n\x02\x61k\x10\x06\x12\x06\n\x02\x61m\x10\x07\x12\x06\n\x02\x61n\x10\x08\x12\x06\n\x02\x61r\x10\t\x12\x06\n\x02\x61s\x10\n\x12\x06\n\x02\x61v\x10\x0b\x12\x06\n\x02\x61y\x10\x0c\x12\x06\n\x02\x61z\x10\r\x12\x06\n\x02\x62\x61\x10\x0e\x12\x06\n\x02\x62\x65\x10\x0f\x12\x06\n\x02\x62g\x10\x10\x12\x06\n\x02\x62h\x10\x11\x12\x06\n\x02\x62i\x10\x12\x12\x06\n\x02\x62m\x10\x13\x12\x06\n\x02\x62n\x10\x14\x12\x06\n\x02\x62o\x10\x15\x12\x06\n\x02\x62r\x10\x16\x12\x06\n\x02\x62s\x10\x17\x12\x06\n\x02\x63\x61\x10\x18\x12\x06\n\x02\x63\x65\x10\x19\x12\x06\n\x02\x63h\x10\x1a\x12\x06\n\x02\x63o\x10\x1b\x12\x06\n\x02\x63r\x10\x1c\x12\x06\n\x02\x63s\x10\x1d\x12\x06\n\x02\x63u\x10\x1e\x12\x06\n\x02\x63v\x10\x1f\x12\x06\n\x02\x63y\x10 \x12\x06\n\x02\x64\x61\x10!\x12\x06\n\x02\x64\x65\x10\"\x12\x06\n\x02\x64v\x10#\x12\x06\n\x02\x64z\x10$\x12\x06\n\x02\x65\x65\x10%\x12\x06\n\x02\x65l\x10&\x12\x06\n\x02\x65o\x10\'\x12\x06\n\x02\x65s\x10(\x12\x06\n\x02\x65t\x10)\x12\x06\n\x02\x65u\x10*\x12\x06\n\x02\x66\x61\x10+\x12\x06\n\x02\x66\x66\x10,\x12\x06\n\x02\x66i\x10-\x12\x06\n\x02\x66j\x10.\x12\x06\n\x02\x66o\x10/\x12\x06\n\x02\x66r\x10\x30\x12\x06\n\x02\x66y\x10\x31\x12\x06\n\x02ga\x10\x32\x12\x06\n\x02gd\x10\x33\x12\x06\n\x02gl\x10\x34\x12\x06\n\x02gn\x10\x35\x12\x06\n\x02gu\x10\x36\x12\x06\n\x02gv\x10\x37\x12\x06\n\x02ha\x10\x38\x12\x06\n\x02he\x10\x39\x12\x06\n\x02hi\x10:\x12\x06\n\x02ho\x10;\x12\x06\n\x02hr\x10<\x12\x06\n\x02ht\x10=\x12\x06\n\x02hu\x10>\x12\x06\n\x02hy\x10?\x12\x06\n\x02hz\x10@\x12\x06\n\x02ia\x10\x41\x12\x06\n\x02id\x10\x42\x12\x06\n\x02ie\x10\x43\x12\x06\n\x02ig\x10\x44\x12\x06\n\x02ii\x10\x45\x12\x06\n\x02ik\x10\x46\x12\x06\n\x02io\x10G\x12\x06\n\x02is\x10H\x12\x06\n\x02it\x10I\x12\x06\n\x02iu\x10J\x12\x06\n\x02ja\x10K\x12\x06\n\x02jv\x10L\x12\x06\n\x02ka\x10M\x12\x06\n\x02kg\x10N\x12\x06\n\x02ki\x10O\x12\x06\n\x02kj\x10P\x12\x06\n\x02kk\x10Q\x12\x06\n\x02kl\x10R\x12\x06\n\x02km\x10S\x12\x06\n\x02kn\x10T\x12\x06\n\x02ko\x10U\x12\x06\n\x02kr\x10V\x12\x06\n\x02ks\x10W\x12\x06\n\x02ku\x10X\x12\x06\n\x02kv\x10Y\x12\x06\n\x02kw\x10Z\x12\x06\n\x02ky\x10[\x12\x06\n\x02la\x10\\\x12\x06\n\x02lb\x10]\x12\x06\n\x02lg\x10^\x12\x06\n\x02li\x10_\x12\x06\n\x02ln\x10`\x12\x06\n\x02lo\x10\x61\x12\x06\n\x02lt\x10\x62\x12\x06\n\x02lu\x10\x63\x12\x06\n\x02lv\x10\x64\x12\x06\n\x02mg\x10\x65\x12\x06\n\x02mh\x10\x66\x12\x06\n\x02mi\x10g\x12\x06\n\x02mk\x10h\x12\x06\n\x02ml\x10i\x12\x06\n\x02mn\x10j\x12\x06\n\x02mr\x10k\x12\x06\n\x02ms\x10l\x12\x06\n\x02mt\x10m\x12\x06\n\x02my\x10n\x12\x06\n\x02na\x10o\x12\x06\n\x02nb\x10p\x12\x06\n\x02nd\x10q\x12\x06\n\x02ne\x10r\x12\x06\n\x02ng\x10s\x12\x06\n\x02nl\x10t\x12\x06\n\x02nn\x10u\x12\x06\n\x02no\x10v\x12\x06\n\x02nr\x10w\x12\x06\n\x02nv\x10x\x12\x06\n\x02ny\x10y\x12\x06\n\x02oc\x10z\x12\x06\n\x02oj\x10{\x12\x06\n\x02om\x10|\x12\x06\n\x02or\x10}\x12\x06\n\x02os\x10~\x12\x06\n\x02pa\x10\x7f\x12\x07\n\x02pi\x10\x80\x01\x12\x07\n\x02pl\x10\x81\x01\x12\x07\n\x02ps\x10\x82\x01\x12\x07\n\x02pt\x10\x83\x01\x12\x07\n\x02qu\x10\x84\x01\x12\x07\n\x02rm\x10\x85\x01\x12\x07\n\x02rn\x10\x86\x01\x12\x07\n\x02ro\x10\x87\x01\x12\x07\n\x02ru\x10\x88\x01\x12\x07\n\x02rw\x10\x89\x01\x12\x07\n\x02sa\x10\x8a\x01\x12\x07\n\x02sc\x10\x8b\x01\x12\x07\n\x02sd\x10\x8c\x01\x12\x07\n\x02se\x10\x8d\x01\x12\x07\n\x02sg\x10\x8e\x01\x12\x07\n\x02si\x10\x8f\x01\x12\x07\n\x02sk\x10\x90\x01\x12\x07\n\x02sl\x10\x91\x01\x12\x07\n\x02sm\x10\x92\x01\x12\x07\n\x02sn\x10\x93\x01\x12\x07\n\x02so\x10\x94\x01\x12\x07\n\x02sq\x10\x95\x01\x12\x07\n\x02sr\x10\x96\x01\x12\x07\n\x02ss\x10\x97\x01\x12\x07\n\x02st\x10\x98\x01\x12\x07\n\x02su\x10\x99\x01\x12\x07\n\x02sv\x10\x9a\x01\x12\x07\n\x02sw\x10\x9b\x01\x12\x07\n\x02ta\x10\x9c\x01\x12\x07\n\x02te\x10\x9d\x01\x12\x07\n\x02tg\x10\x9e\x01\x12\x07\n\x02th\x10\x9f\x01\x12\x07\n\x02ti\x10\xa0\x01\x12\x07\n\x02tk\x10\xa1\x01\x12\x07\n\x02tl\x10\xa2\x01\x12\x07\n\x02tn\x10\xa3\x01\x12\x07\n\x02to\x10\xa4\x01\x12\x07\n\x02tr\x10\xa5\x01\x12\x07\n\x02ts\x10\xa6\x01\x12\x07\n\x02tt\x10\xa7\x01\x12\x07\n\x02tw\x10\xa8\x01\x12\x07\n\x02ty\x10\xa9\x01\x12\x07\n\x02ug\x10\xaa\x01\x12\x07\n\x02uk\x10\xab\x01\x12\x07\n\x02ur\x10\xac\x01\x12\x07\n\x02uz\x10\xad\x01\x12\x07\n\x02ve\x10\xae\x01\x12\x07\n\x02vi\x10\xaf\x01\x12\x07\n\x02vo\x10\xb0\x01\x12\x07\n\x02wa\x10\xb1\x01\x12\x07\n\x02wo\x10\xb2\x01\x12\x07\n\x02xh\x10\xb3\x01\x12\x07\n\x02yi\x10\xb4\x01\x12\x07\n\x02yo\x10\xb5\x01\x12\x07\n\x02za\x10\xb6\x01\x12\x07\n\x02zh\x10\xb7\x01\x12\x07\n\x02zu\x10\xb8\x01\"\xaa\x10\n\x06Script\x12\x12\n\x0eUNKNOWN_SCRIPT\x10\x00\x12\x08\n\x04\x41\x64lm\x10\x01\x12\x08\n\x04\x41\x66\x61k\x10\x02\x12\x08\n\x04\x41ghb\x10\x03\x12\x08\n\x04\x41hom\x10\x04\x12\x08\n\x04\x41rab\x10\x05\x12\x08\n\x04\x41ran\x10\x06\x12\x08\n\x04\x41rmi\x10\x07\x12\x08\n\x04\x41rmn\x10\x08\x12\x08\n\x04\x41vst\x10\t\x12\x08\n\x04\x42\x61li\x10\n\x12\x08\n\x04\x42\x61mu\x10\x0b\x12\x08\n\x04\x42\x61ss\x10\x0c\x12\x08\n\x04\x42\x61tk\x10\r\x12\x08\n\x04\x42\x65ng\x10\x0e\x12\x08\n\x04\x42hks\x10\x0f\x12\x08\n\x04\x42lis\x10\x10\x12\x08\n\x04\x42opo\x10\x11\x12\x08\n\x04\x42rah\x10\x12\x12\x08\n\x04\x42rai\x10\x13\x12\x08\n\x04\x42ugi\x10\x14\x12\x08\n\x04\x42uhd\x10\x15\x12\x08\n\x04\x43\x61km\x10\x16\x12\x08\n\x04\x43\x61ns\x10\x17\x12\x08\n\x04\x43\x61ri\x10\x18\x12\x08\n\x04\x43ham\x10\x19\x12\x08\n\x04\x43her\x10\x1a\x12\x08\n\x04\x43irt\x10\x1b\x12\x08\n\x04\x43opt\x10\x1c\x12\x08\n\x04\x43pmn\x10\x1d\x12\x08\n\x04\x43prt\x10\x1e\x12\x08\n\x04\x43yrl\x10\x1f\x12\x08\n\x04\x43yrs\x10 \x12\x08\n\x04\x44\x65va\x10!\x12\x08\n\x04\x44ogr\x10\"\x12\x08\n\x04\x44srt\x10#\x12\x08\n\x04\x44upl\x10$\x12\x08\n\x04\x45gyd\x10%\x12\x08\n\x04\x45gyh\x10&\x12\x08\n\x04\x45gyp\x10\'\x12\x08\n\x04\x45lba\x10(\x12\x08\n\x04\x45lym\x10)\x12\x08\n\x04\x45thi\x10*\x12\x08\n\x04Geok\x10+\x12\x08\n\x04Geor\x10,\x12\x08\n\x04Glag\x10-\x12\x08\n\x04Gong\x10.\x12\x08\n\x04Gonm\x10/\x12\x08\n\x04Goth\x10\x30\x12\x08\n\x04Gran\x10\x31\x12\x08\n\x04Grek\x10\x32\x12\x08\n\x04Gujr\x10\x33\x12\x08\n\x04Guru\x10\x34\x12\x08\n\x04Hanb\x10\x35\x12\x08\n\x04Hang\x10\x36\x12\x08\n\x04Hani\x10\x37\x12\x08\n\x04Hano\x10\x38\x12\x08\n\x04Hans\x10\x39\x12\x08\n\x04Hant\x10:\x12\x08\n\x04Hatr\x10;\x12\x08\n\x04Hebr\x10<\x12\x08\n\x04Hira\x10=\x12\x08\n\x04Hluw\x10>\x12\x08\n\x04Hmng\x10?\x12\x08\n\x04Hmnp\x10@\x12\x08\n\x04Hrkt\x10\x41\x12\x08\n\x04Hung\x10\x42\x12\x08\n\x04Inds\x10\x43\x12\x08\n\x04Ital\x10\x44\x12\x08\n\x04Jamo\x10\x45\x12\x08\n\x04Java\x10\x46\x12\x08\n\x04Jpan\x10G\x12\x08\n\x04Jurc\x10H\x12\x08\n\x04Kali\x10I\x12\x08\n\x04Kana\x10J\x12\x08\n\x04Khar\x10K\x12\x08\n\x04Khmr\x10L\x12\x08\n\x04Khoj\x10M\x12\x08\n\x04Kitl\x10N\x12\x08\n\x04Kits\x10O\x12\x08\n\x04Knda\x10P\x12\x08\n\x04Kore\x10Q\x12\x08\n\x04Kpel\x10R\x12\x08\n\x04Kthi\x10S\x12\x08\n\x04Lana\x10T\x12\x08\n\x04Laoo\x10U\x12\x08\n\x04Latf\x10V\x12\x08\n\x04Latg\x10W\x12\x08\n\x04Latn\x10X\x12\x08\n\x04Leke\x10Y\x12\x08\n\x04Lepc\x10Z\x12\x08\n\x04Limb\x10[\x12\x08\n\x04Lina\x10\\\x12\x08\n\x04Linb\x10]\x12\x08\n\x04Lisu\x10^\x12\x08\n\x04Loma\x10_\x12\x08\n\x04Lyci\x10`\x12\x08\n\x04Lydi\x10\x61\x12\x08\n\x04Mahj\x10\x62\x12\x08\n\x04Maka\x10\x63\x12\x08\n\x04Mand\x10\x64\x12\x08\n\x04Mani\x10\x65\x12\x08\n\x04Marc\x10\x66\x12\x08\n\x04Maya\x10g\x12\x08\n\x04Medf\x10h\x12\x08\n\x04Mend\x10i\x12\x08\n\x04Merc\x10j\x12\x08\n\x04Mero\x10k\x12\x08\n\x04Mlym\x10l\x12\x08\n\x04Modi\x10m\x12\x08\n\x04Mong\x10n\x12\x08\n\x04Moon\x10o\x12\x08\n\x04Mroo\x10p\x12\x08\n\x04Mtei\x10q\x12\x08\n\x04Mult\x10r\x12\x08\n\x04Mymr\x10s\x12\x08\n\x04Nand\x10t\x12\x08\n\x04Narb\x10u\x12\x08\n\x04Nbat\x10v\x12\x08\n\x04Newa\x10w\x12\x08\n\x04Nkdb\x10x\x12\x08\n\x04Nkgb\x10y\x12\x08\n\x04Nkoo\x10z\x12\x08\n\x04Nshu\x10{\x12\x08\n\x04Ogam\x10|\x12\x08\n\x04Olck\x10}\x12\x08\n\x04Orkh\x10~\x12\x08\n\x04Orya\x10\x7f\x12\t\n\x04Osge\x10\x80\x01\x12\t\n\x04Osma\x10\x81\x01\x12\t\n\x04Palm\x10\x82\x01\x12\t\n\x04Pauc\x10\x83\x01\x12\t\n\x04Perm\x10\x84\x01\x12\t\n\x04Phag\x10\x85\x01\x12\t\n\x04Phli\x10\x86\x01\x12\t\n\x04Phlp\x10\x87\x01\x12\t\n\x04Phlv\x10\x88\x01\x12\t\n\x04Phnx\x10\x89\x01\x12\t\n\x04Plrd\x10\x8a\x01\x12\t\n\x04Piqd\x10\x8b\x01\x12\t\n\x04Prti\x10\x8c\x01\x12\t\n\x04Qaaa\x10\x8d\x01\x12\t\n\x04Qabx\x10\x8e\x01\x12\t\n\x04Rjng\x10\x8f\x01\x12\t\n\x04Rohg\x10\x90\x01\x12\t\n\x04Roro\x10\x91\x01\x12\t\n\x04Runr\x10\x92\x01\x12\t\n\x04Samr\x10\x93\x01\x12\t\n\x04Sara\x10\x94\x01\x12\t\n\x04Sarb\x10\x95\x01\x12\t\n\x04Saur\x10\x96\x01\x12\t\n\x04Sgnw\x10\x97\x01\x12\t\n\x04Shaw\x10\x98\x01\x12\t\n\x04Shrd\x10\x99\x01\x12\t\n\x04Shui\x10\x9a\x01\x12\t\n\x04Sidd\x10\x9b\x01\x12\t\n\x04Sind\x10\x9c\x01\x12\t\n\x04Sinh\x10\x9d\x01\x12\t\n\x04Sogd\x10\x9e\x01\x12\t\n\x04Sogo\x10\x9f\x01\x12\t\n\x04Sora\x10\xa0\x01\x12\t\n\x04Soyo\x10\xa1\x01\x12\t\n\x04Sund\x10\xa2\x01\x12\t\n\x04Sylo\x10\xa3\x01\x12\t\n\x04Syrc\x10\xa4\x01\x12\t\n\x04Syre\x10\xa5\x01\x12\t\n\x04Syrj\x10\xa6\x01\x12\t\n\x04Syrn\x10\xa7\x01\x12\t\n\x04Tagb\x10\xa8\x01\x12\t\n\x04Takr\x10\xa9\x01\x12\t\n\x04Tale\x10\xaa\x01\x12\t\n\x04Talu\x10\xab\x01\x12\t\n\x04Taml\x10\xac\x01\x12\t\n\x04Tang\x10\xad\x01\x12\t\n\x04Tavt\x10\xae\x01\x12\t\n\x04Telu\x10\xaf\x01\x12\t\n\x04Teng\x10\xb0\x01\x12\t\n\x04Tfng\x10\xb1\x01\x12\t\n\x04Tglg\x10\xb2\x01\x12\t\n\x04Thaa\x10\xb3\x01\x12\t\n\x04Thai\x10\xb4\x01\x12\t\n\x04Tibt\x10\xb5\x01\x12\t\n\x04Tirh\x10\xb6\x01\x12\t\n\x04Ugar\x10\xb7\x01\x12\t\n\x04Vaii\x10\xb8\x01\x12\t\n\x04Visp\x10\xb9\x01\x12\t\n\x04Wara\x10\xba\x01\x12\t\n\x04Wcho\x10\xbb\x01\x12\t\n\x04Wole\x10\xbc\x01\x12\t\n\x04Xpeo\x10\xbd\x01\x12\t\n\x04Xsux\x10\xbe\x01\x12\t\n\x04Yiii\x10\xbf\x01\x12\t\n\x04Zanb\x10\xc0\x01\x12\t\n\x04Zinh\x10\xc1\x01\x12\t\n\x04Zmth\x10\xc2\x01\x12\t\n\x04Zsye\x10\xc3\x01\x12\t\n\x04Zsym\x10\xc4\x01\x12\t\n\x04Zxxx\x10\xc5\x01\x12\t\n\x04Zyyy\x10\xc6\x01\x12\t\n\x04Zzzz\x10\xc7\x01\"\xe4\x11\n\x08Location\x12%\n\x07\x63ountry\x18\x01 \x01(\x0e\x32\x14.pb.Location.Country\x12\r\n\x05state\x18\x02 \x01(\t\x12\x0c\n\x04\x63ity\x18\x03 \x01(\t\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\x12\x10\n\x08latitude\x18\x05 \x01(\x11\x12\x11\n\tlongitude\x18\x06 \x01(\x11\"\xe0\x10\n\x07\x43ountry\x12\x13\n\x0fUNKNOWN_COUNTRY\x10\x00\x12\x06\n\x02\x41\x46\x10\x01\x12\x06\n\x02\x41X\x10\x02\x12\x06\n\x02\x41L\x10\x03\x12\x06\n\x02\x44Z\x10\x04\x12\x06\n\x02\x41S\x10\x05\x12\x06\n\x02\x41\x44\x10\x06\x12\x06\n\x02\x41O\x10\x07\x12\x06\n\x02\x41I\x10\x08\x12\x06\n\x02\x41Q\x10\t\x12\x06\n\x02\x41G\x10\n\x12\x06\n\x02\x41R\x10\x0b\x12\x06\n\x02\x41M\x10\x0c\x12\x06\n\x02\x41W\x10\r\x12\x06\n\x02\x41U\x10\x0e\x12\x06\n\x02\x41T\x10\x0f\x12\x06\n\x02\x41Z\x10\x10\x12\x06\n\x02\x42S\x10\x11\x12\x06\n\x02\x42H\x10\x12\x12\x06\n\x02\x42\x44\x10\x13\x12\x06\n\x02\x42\x42\x10\x14\x12\x06\n\x02\x42Y\x10\x15\x12\x06\n\x02\x42\x45\x10\x16\x12\x06\n\x02\x42Z\x10\x17\x12\x06\n\x02\x42J\x10\x18\x12\x06\n\x02\x42M\x10\x19\x12\x06\n\x02\x42T\x10\x1a\x12\x06\n\x02\x42O\x10\x1b\x12\x06\n\x02\x42Q\x10\x1c\x12\x06\n\x02\x42\x41\x10\x1d\x12\x06\n\x02\x42W\x10\x1e\x12\x06\n\x02\x42V\x10\x1f\x12\x06\n\x02\x42R\x10 \x12\x06\n\x02IO\x10!\x12\x06\n\x02\x42N\x10\"\x12\x06\n\x02\x42G\x10#\x12\x06\n\x02\x42\x46\x10$\x12\x06\n\x02\x42I\x10%\x12\x06\n\x02KH\x10&\x12\x06\n\x02\x43M\x10\'\x12\x06\n\x02\x43\x41\x10(\x12\x06\n\x02\x43V\x10)\x12\x06\n\x02KY\x10*\x12\x06\n\x02\x43\x46\x10+\x12\x06\n\x02TD\x10,\x12\x06\n\x02\x43L\x10-\x12\x06\n\x02\x43N\x10.\x12\x06\n\x02\x43X\x10/\x12\x06\n\x02\x43\x43\x10\x30\x12\x06\n\x02\x43O\x10\x31\x12\x06\n\x02KM\x10\x32\x12\x06\n\x02\x43G\x10\x33\x12\x06\n\x02\x43\x44\x10\x34\x12\x06\n\x02\x43K\x10\x35\x12\x06\n\x02\x43R\x10\x36\x12\x06\n\x02\x43I\x10\x37\x12\x06\n\x02HR\x10\x38\x12\x06\n\x02\x43U\x10\x39\x12\x06\n\x02\x43W\x10:\x12\x06\n\x02\x43Y\x10;\x12\x06\n\x02\x43Z\x10<\x12\x06\n\x02\x44K\x10=\x12\x06\n\x02\x44J\x10>\x12\x06\n\x02\x44M\x10?\x12\x06\n\x02\x44O\x10@\x12\x06\n\x02\x45\x43\x10\x41\x12\x06\n\x02\x45G\x10\x42\x12\x06\n\x02SV\x10\x43\x12\x06\n\x02GQ\x10\x44\x12\x06\n\x02\x45R\x10\x45\x12\x06\n\x02\x45\x45\x10\x46\x12\x06\n\x02\x45T\x10G\x12\x06\n\x02\x46K\x10H\x12\x06\n\x02\x46O\x10I\x12\x06\n\x02\x46J\x10J\x12\x06\n\x02\x46I\x10K\x12\x06\n\x02\x46R\x10L\x12\x06\n\x02GF\x10M\x12\x06\n\x02PF\x10N\x12\x06\n\x02TF\x10O\x12\x06\n\x02GA\x10P\x12\x06\n\x02GM\x10Q\x12\x06\n\x02GE\x10R\x12\x06\n\x02\x44\x45\x10S\x12\x06\n\x02GH\x10T\x12\x06\n\x02GI\x10U\x12\x06\n\x02GR\x10V\x12\x06\n\x02GL\x10W\x12\x06\n\x02GD\x10X\x12\x06\n\x02GP\x10Y\x12\x06\n\x02GU\x10Z\x12\x06\n\x02GT\x10[\x12\x06\n\x02GG\x10\\\x12\x06\n\x02GN\x10]\x12\x06\n\x02GW\x10^\x12\x06\n\x02GY\x10_\x12\x06\n\x02HT\x10`\x12\x06\n\x02HM\x10\x61\x12\x06\n\x02VA\x10\x62\x12\x06\n\x02HN\x10\x63\x12\x06\n\x02HK\x10\x64\x12\x06\n\x02HU\x10\x65\x12\x06\n\x02IS\x10\x66\x12\x06\n\x02IN\x10g\x12\x06\n\x02ID\x10h\x12\x06\n\x02IR\x10i\x12\x06\n\x02IQ\x10j\x12\x06\n\x02IE\x10k\x12\x06\n\x02IM\x10l\x12\x06\n\x02IL\x10m\x12\x06\n\x02IT\x10n\x12\x06\n\x02JM\x10o\x12\x06\n\x02JP\x10p\x12\x06\n\x02JE\x10q\x12\x06\n\x02JO\x10r\x12\x06\n\x02KZ\x10s\x12\x06\n\x02KE\x10t\x12\x06\n\x02KI\x10u\x12\x06\n\x02KP\x10v\x12\x06\n\x02KR\x10w\x12\x06\n\x02KW\x10x\x12\x06\n\x02KG\x10y\x12\x06\n\x02LA\x10z\x12\x06\n\x02LV\x10{\x12\x06\n\x02LB\x10|\x12\x06\n\x02LS\x10}\x12\x06\n\x02LR\x10~\x12\x06\n\x02LY\x10\x7f\x12\x07\n\x02LI\x10\x80\x01\x12\x07\n\x02LT\x10\x81\x01\x12\x07\n\x02LU\x10\x82\x01\x12\x07\n\x02MO\x10\x83\x01\x12\x07\n\x02MK\x10\x84\x01\x12\x07\n\x02MG\x10\x85\x01\x12\x07\n\x02MW\x10\x86\x01\x12\x07\n\x02MY\x10\x87\x01\x12\x07\n\x02MV\x10\x88\x01\x12\x07\n\x02ML\x10\x89\x01\x12\x07\n\x02MT\x10\x8a\x01\x12\x07\n\x02MH\x10\x8b\x01\x12\x07\n\x02MQ\x10\x8c\x01\x12\x07\n\x02MR\x10\x8d\x01\x12\x07\n\x02MU\x10\x8e\x01\x12\x07\n\x02YT\x10\x8f\x01\x12\x07\n\x02MX\x10\x90\x01\x12\x07\n\x02\x46M\x10\x91\x01\x12\x07\n\x02MD\x10\x92\x01\x12\x07\n\x02MC\x10\x93\x01\x12\x07\n\x02MN\x10\x94\x01\x12\x07\n\x02ME\x10\x95\x01\x12\x07\n\x02MS\x10\x96\x01\x12\x07\n\x02MA\x10\x97\x01\x12\x07\n\x02MZ\x10\x98\x01\x12\x07\n\x02MM\x10\x99\x01\x12\x07\n\x02NA\x10\x9a\x01\x12\x07\n\x02NR\x10\x9b\x01\x12\x07\n\x02NP\x10\x9c\x01\x12\x07\n\x02NL\x10\x9d\x01\x12\x07\n\x02NC\x10\x9e\x01\x12\x07\n\x02NZ\x10\x9f\x01\x12\x07\n\x02NI\x10\xa0\x01\x12\x07\n\x02NE\x10\xa1\x01\x12\x07\n\x02NG\x10\xa2\x01\x12\x07\n\x02NU\x10\xa3\x01\x12\x07\n\x02NF\x10\xa4\x01\x12\x07\n\x02MP\x10\xa5\x01\x12\x07\n\x02NO\x10\xa6\x01\x12\x07\n\x02OM\x10\xa7\x01\x12\x07\n\x02PK\x10\xa8\x01\x12\x07\n\x02PW\x10\xa9\x01\x12\x07\n\x02PS\x10\xaa\x01\x12\x07\n\x02PA\x10\xab\x01\x12\x07\n\x02PG\x10\xac\x01\x12\x07\n\x02PY\x10\xad\x01\x12\x07\n\x02PE\x10\xae\x01\x12\x07\n\x02PH\x10\xaf\x01\x12\x07\n\x02PN\x10\xb0\x01\x12\x07\n\x02PL\x10\xb1\x01\x12\x07\n\x02PT\x10\xb2\x01\x12\x07\n\x02PR\x10\xb3\x01\x12\x07\n\x02QA\x10\xb4\x01\x12\x07\n\x02RE\x10\xb5\x01\x12\x07\n\x02RO\x10\xb6\x01\x12\x07\n\x02RU\x10\xb7\x01\x12\x07\n\x02RW\x10\xb8\x01\x12\x07\n\x02\x42L\x10\xb9\x01\x12\x07\n\x02SH\x10\xba\x01\x12\x07\n\x02KN\x10\xbb\x01\x12\x07\n\x02LC\x10\xbc\x01\x12\x07\n\x02MF\x10\xbd\x01\x12\x07\n\x02PM\x10\xbe\x01\x12\x07\n\x02VC\x10\xbf\x01\x12\x07\n\x02WS\x10\xc0\x01\x12\x07\n\x02SM\x10\xc1\x01\x12\x07\n\x02ST\x10\xc2\x01\x12\x07\n\x02SA\x10\xc3\x01\x12\x07\n\x02SN\x10\xc4\x01\x12\x07\n\x02RS\x10\xc5\x01\x12\x07\n\x02SC\x10\xc6\x01\x12\x07\n\x02SL\x10\xc7\x01\x12\x07\n\x02SG\x10\xc8\x01\x12\x07\n\x02SX\x10\xc9\x01\x12\x07\n\x02SK\x10\xca\x01\x12\x07\n\x02SI\x10\xcb\x01\x12\x07\n\x02SB\x10\xcc\x01\x12\x07\n\x02SO\x10\xcd\x01\x12\x07\n\x02ZA\x10\xce\x01\x12\x07\n\x02GS\x10\xcf\x01\x12\x07\n\x02SS\x10\xd0\x01\x12\x07\n\x02\x45S\x10\xd1\x01\x12\x07\n\x02LK\x10\xd2\x01\x12\x07\n\x02SD\x10\xd3\x01\x12\x07\n\x02SR\x10\xd4\x01\x12\x07\n\x02SJ\x10\xd5\x01\x12\x07\n\x02SZ\x10\xd6\x01\x12\x07\n\x02SE\x10\xd7\x01\x12\x07\n\x02\x43H\x10\xd8\x01\x12\x07\n\x02SY\x10\xd9\x01\x12\x07\n\x02TW\x10\xda\x01\x12\x07\n\x02TJ\x10\xdb\x01\x12\x07\n\x02TZ\x10\xdc\x01\x12\x07\n\x02TH\x10\xdd\x01\x12\x07\n\x02TL\x10\xde\x01\x12\x07\n\x02TG\x10\xdf\x01\x12\x07\n\x02TK\x10\xe0\x01\x12\x07\n\x02TO\x10\xe1\x01\x12\x07\n\x02TT\x10\xe2\x01\x12\x07\n\x02TN\x10\xe3\x01\x12\x07\n\x02TR\x10\xe4\x01\x12\x07\n\x02TM\x10\xe5\x01\x12\x07\n\x02TC\x10\xe6\x01\x12\x07\n\x02TV\x10\xe7\x01\x12\x07\n\x02UG\x10\xe8\x01\x12\x07\n\x02UA\x10\xe9\x01\x12\x07\n\x02\x41\x45\x10\xea\x01\x12\x07\n\x02GB\x10\xeb\x01\x12\x07\n\x02US\x10\xec\x01\x12\x07\n\x02UM\x10\xed\x01\x12\x07\n\x02UY\x10\xee\x01\x12\x07\n\x02UZ\x10\xef\x01\x12\x07\n\x02VU\x10\xf0\x01\x12\x07\n\x02VE\x10\xf1\x01\x12\x07\n\x02VN\x10\xf2\x01\x12\x07\n\x02VG\x10\xf3\x01\x12\x07\n\x02VI\x10\xf4\x01\x12\x07\n\x02WF\x10\xf5\x01\x12\x07\n\x02\x45H\x10\xf6\x01\x12\x07\n\x02YE\x10\xf7\x01\x12\x07\n\x02ZM\x10\xf8\x01\x12\x07\n\x02ZW\x10\xf9\x01\x62\x06proto3') + serialized_pb=_b('\n\x0b\x63laim.proto\x12\x02pb\"\xab\x02\n\x05\x43laim\x12\x1c\n\x06stream\x18\x01 \x01(\x0b\x32\n.pb.StreamH\x00\x12\x1e\n\x07\x63hannel\x18\x02 \x01(\x0b\x32\x0b.pb.ChannelH\x00\x12#\n\ncollection\x18\x03 \x01(\x0b\x32\r.pb.ClaimListH\x00\x12$\n\x06repost\x18\x04 \x01(\x0b\x32\x12.pb.ClaimReferenceH\x00\x12\r\n\x05title\x18\x08 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\t \x01(\t\x12\x1d\n\tthumbnail\x18\n \x01(\x0b\x32\n.pb.Source\x12\x0c\n\x04tags\x18\x0b \x03(\t\x12\x1f\n\tlanguages\x18\x0c \x03(\x0b\x32\x0c.pb.Language\x12\x1f\n\tlocations\x18\r \x03(\x0b\x32\x0c.pb.LocationB\x06\n\x04type\"\x84\x02\n\x06Stream\x12\x1a\n\x06source\x18\x01 \x01(\x0b\x32\n.pb.Source\x12\x0e\n\x06\x61uthor\x18\x02 \x01(\t\x12\x0f\n\x07license\x18\x03 \x01(\t\x12\x13\n\x0blicense_url\x18\x04 \x01(\t\x12\x14\n\x0crelease_time\x18\x05 \x01(\x03\x12\x14\n\x03\x66\x65\x65\x18\x06 \x01(\x0b\x32\x07.pb.Fee\x12\x1a\n\x05image\x18\n \x01(\x0b\x32\t.pb.ImageH\x00\x12\x1a\n\x05video\x18\x0b \x01(\x0b\x32\t.pb.VideoH\x00\x12\x1a\n\x05\x61udio\x18\x0c \x01(\x0b\x32\t.pb.AudioH\x00\x12 \n\x08software\x18\r \x01(\x0b\x32\x0c.pb.SoftwareH\x00\x42\x06\n\x04type\"}\n\x07\x43hannel\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x13\n\x0bwebsite_url\x18\x03 \x01(\t\x12\x19\n\x05\x63over\x18\x04 \x01(\x0b\x32\n.pb.Source\x12\x1f\n\x08\x66\x65\x61tured\x18\x05 \x01(\x0b\x32\r.pb.ClaimList\"$\n\x0e\x43laimReference\x12\x12\n\nclaim_hash\x18\x01 \x01(\x0c\"\xa7\x01\n\tClaimList\x12)\n\tlist_type\x18\x01 \x01(\x0e\x32\x16.pb.ClaimList.ListType\x12,\n\x10\x63laim_references\x18\x02 \x03(\x0b\x32\x12.pb.ClaimReference\"A\n\x08ListType\x12\x15\n\x11UNKNOWN_LIST_TYPE\x10\x00\x12\x0e\n\nCOLLECTION\x10\x01\x12\x0e\n\nDERIVATION\x10\x02\"d\n\x06Source\x12\x0c\n\x04hash\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x04\x12\x12\n\nmedia_type\x18\x04 \x01(\t\x12\x0b\n\x03url\x18\x05 \x01(\t\x12\x0f\n\x07sd_hash\x18\x06 \x01(\x0c\"\x87\x01\n\x03\x46\x65\x65\x12\"\n\x08\x63urrency\x18\x01 \x01(\x0e\x32\x10.pb.Fee.Currency\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\";\n\x08\x43urrency\x12\x14\n\x10UNKNOWN_CURRENCY\x10\x00\x12\x07\n\x03LBC\x10\x01\x12\x07\n\x03\x42TC\x10\x02\x12\x07\n\x03USD\x10\x03\"&\n\x05Image\x12\r\n\x05width\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\r\"R\n\x05Video\x12\r\n\x05width\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\r\x12\x10\n\x08\x64uration\x18\x03 \x01(\r\x12\x18\n\x05\x61udio\x18\x0f \x01(\x0b\x32\t.pb.Audio\"\x19\n\x05\x41udio\x12\x10\n\x08\x64uration\x18\x01 \x01(\r\"l\n\x08Software\x12\n\n\x02os\x18\x01 \x01(\t\"T\n\x02OS\x12\x0e\n\nUNKNOWN_OS\x10\x00\x12\x07\n\x03\x41NY\x10\x01\x12\t\n\x05LINUX\x10\x02\x12\x0b\n\x07WINDOWS\x10\x03\x12\x07\n\x03MAC\x10\x04\x12\x0b\n\x07\x41NDROID\x10\x05\x12\x07\n\x03IOS\x10\x06\"\xc7\x1d\n\x08Language\x12\'\n\x08language\x18\x01 \x01(\x0e\x32\x15.pb.Language.Language\x12#\n\x06script\x18\x02 \x01(\x0e\x32\x13.pb.Language.Script\x12$\n\x06region\x18\x03 \x01(\x0e\x32\x14.pb.Location.Country\"\x99\x0c\n\x08Language\x12\x14\n\x10UNKNOWN_LANGUAGE\x10\x00\x12\x06\n\x02\x65n\x10\x01\x12\x06\n\x02\x61\x61\x10\x02\x12\x06\n\x02\x61\x62\x10\x03\x12\x06\n\x02\x61\x65\x10\x04\x12\x06\n\x02\x61\x66\x10\x05\x12\x06\n\x02\x61k\x10\x06\x12\x06\n\x02\x61m\x10\x07\x12\x06\n\x02\x61n\x10\x08\x12\x06\n\x02\x61r\x10\t\x12\x06\n\x02\x61s\x10\n\x12\x06\n\x02\x61v\x10\x0b\x12\x06\n\x02\x61y\x10\x0c\x12\x06\n\x02\x61z\x10\r\x12\x06\n\x02\x62\x61\x10\x0e\x12\x06\n\x02\x62\x65\x10\x0f\x12\x06\n\x02\x62g\x10\x10\x12\x06\n\x02\x62h\x10\x11\x12\x06\n\x02\x62i\x10\x12\x12\x06\n\x02\x62m\x10\x13\x12\x06\n\x02\x62n\x10\x14\x12\x06\n\x02\x62o\x10\x15\x12\x06\n\x02\x62r\x10\x16\x12\x06\n\x02\x62s\x10\x17\x12\x06\n\x02\x63\x61\x10\x18\x12\x06\n\x02\x63\x65\x10\x19\x12\x06\n\x02\x63h\x10\x1a\x12\x06\n\x02\x63o\x10\x1b\x12\x06\n\x02\x63r\x10\x1c\x12\x06\n\x02\x63s\x10\x1d\x12\x06\n\x02\x63u\x10\x1e\x12\x06\n\x02\x63v\x10\x1f\x12\x06\n\x02\x63y\x10 \x12\x06\n\x02\x64\x61\x10!\x12\x06\n\x02\x64\x65\x10\"\x12\x06\n\x02\x64v\x10#\x12\x06\n\x02\x64z\x10$\x12\x06\n\x02\x65\x65\x10%\x12\x06\n\x02\x65l\x10&\x12\x06\n\x02\x65o\x10\'\x12\x06\n\x02\x65s\x10(\x12\x06\n\x02\x65t\x10)\x12\x06\n\x02\x65u\x10*\x12\x06\n\x02\x66\x61\x10+\x12\x06\n\x02\x66\x66\x10,\x12\x06\n\x02\x66i\x10-\x12\x06\n\x02\x66j\x10.\x12\x06\n\x02\x66o\x10/\x12\x06\n\x02\x66r\x10\x30\x12\x06\n\x02\x66y\x10\x31\x12\x06\n\x02ga\x10\x32\x12\x06\n\x02gd\x10\x33\x12\x06\n\x02gl\x10\x34\x12\x06\n\x02gn\x10\x35\x12\x06\n\x02gu\x10\x36\x12\x06\n\x02gv\x10\x37\x12\x06\n\x02ha\x10\x38\x12\x06\n\x02he\x10\x39\x12\x06\n\x02hi\x10:\x12\x06\n\x02ho\x10;\x12\x06\n\x02hr\x10<\x12\x06\n\x02ht\x10=\x12\x06\n\x02hu\x10>\x12\x06\n\x02hy\x10?\x12\x06\n\x02hz\x10@\x12\x06\n\x02ia\x10\x41\x12\x06\n\x02id\x10\x42\x12\x06\n\x02ie\x10\x43\x12\x06\n\x02ig\x10\x44\x12\x06\n\x02ii\x10\x45\x12\x06\n\x02ik\x10\x46\x12\x06\n\x02io\x10G\x12\x06\n\x02is\x10H\x12\x06\n\x02it\x10I\x12\x06\n\x02iu\x10J\x12\x06\n\x02ja\x10K\x12\x06\n\x02jv\x10L\x12\x06\n\x02ka\x10M\x12\x06\n\x02kg\x10N\x12\x06\n\x02ki\x10O\x12\x06\n\x02kj\x10P\x12\x06\n\x02kk\x10Q\x12\x06\n\x02kl\x10R\x12\x06\n\x02km\x10S\x12\x06\n\x02kn\x10T\x12\x06\n\x02ko\x10U\x12\x06\n\x02kr\x10V\x12\x06\n\x02ks\x10W\x12\x06\n\x02ku\x10X\x12\x06\n\x02kv\x10Y\x12\x06\n\x02kw\x10Z\x12\x06\n\x02ky\x10[\x12\x06\n\x02la\x10\\\x12\x06\n\x02lb\x10]\x12\x06\n\x02lg\x10^\x12\x06\n\x02li\x10_\x12\x06\n\x02ln\x10`\x12\x06\n\x02lo\x10\x61\x12\x06\n\x02lt\x10\x62\x12\x06\n\x02lu\x10\x63\x12\x06\n\x02lv\x10\x64\x12\x06\n\x02mg\x10\x65\x12\x06\n\x02mh\x10\x66\x12\x06\n\x02mi\x10g\x12\x06\n\x02mk\x10h\x12\x06\n\x02ml\x10i\x12\x06\n\x02mn\x10j\x12\x06\n\x02mr\x10k\x12\x06\n\x02ms\x10l\x12\x06\n\x02mt\x10m\x12\x06\n\x02my\x10n\x12\x06\n\x02na\x10o\x12\x06\n\x02nb\x10p\x12\x06\n\x02nd\x10q\x12\x06\n\x02ne\x10r\x12\x06\n\x02ng\x10s\x12\x06\n\x02nl\x10t\x12\x06\n\x02nn\x10u\x12\x06\n\x02no\x10v\x12\x06\n\x02nr\x10w\x12\x06\n\x02nv\x10x\x12\x06\n\x02ny\x10y\x12\x06\n\x02oc\x10z\x12\x06\n\x02oj\x10{\x12\x06\n\x02om\x10|\x12\x06\n\x02or\x10}\x12\x06\n\x02os\x10~\x12\x06\n\x02pa\x10\x7f\x12\x07\n\x02pi\x10\x80\x01\x12\x07\n\x02pl\x10\x81\x01\x12\x07\n\x02ps\x10\x82\x01\x12\x07\n\x02pt\x10\x83\x01\x12\x07\n\x02qu\x10\x84\x01\x12\x07\n\x02rm\x10\x85\x01\x12\x07\n\x02rn\x10\x86\x01\x12\x07\n\x02ro\x10\x87\x01\x12\x07\n\x02ru\x10\x88\x01\x12\x07\n\x02rw\x10\x89\x01\x12\x07\n\x02sa\x10\x8a\x01\x12\x07\n\x02sc\x10\x8b\x01\x12\x07\n\x02sd\x10\x8c\x01\x12\x07\n\x02se\x10\x8d\x01\x12\x07\n\x02sg\x10\x8e\x01\x12\x07\n\x02si\x10\x8f\x01\x12\x07\n\x02sk\x10\x90\x01\x12\x07\n\x02sl\x10\x91\x01\x12\x07\n\x02sm\x10\x92\x01\x12\x07\n\x02sn\x10\x93\x01\x12\x07\n\x02so\x10\x94\x01\x12\x07\n\x02sq\x10\x95\x01\x12\x07\n\x02sr\x10\x96\x01\x12\x07\n\x02ss\x10\x97\x01\x12\x07\n\x02st\x10\x98\x01\x12\x07\n\x02su\x10\x99\x01\x12\x07\n\x02sv\x10\x9a\x01\x12\x07\n\x02sw\x10\x9b\x01\x12\x07\n\x02ta\x10\x9c\x01\x12\x07\n\x02te\x10\x9d\x01\x12\x07\n\x02tg\x10\x9e\x01\x12\x07\n\x02th\x10\x9f\x01\x12\x07\n\x02ti\x10\xa0\x01\x12\x07\n\x02tk\x10\xa1\x01\x12\x07\n\x02tl\x10\xa2\x01\x12\x07\n\x02tn\x10\xa3\x01\x12\x07\n\x02to\x10\xa4\x01\x12\x07\n\x02tr\x10\xa5\x01\x12\x07\n\x02ts\x10\xa6\x01\x12\x07\n\x02tt\x10\xa7\x01\x12\x07\n\x02tw\x10\xa8\x01\x12\x07\n\x02ty\x10\xa9\x01\x12\x07\n\x02ug\x10\xaa\x01\x12\x07\n\x02uk\x10\xab\x01\x12\x07\n\x02ur\x10\xac\x01\x12\x07\n\x02uz\x10\xad\x01\x12\x07\n\x02ve\x10\xae\x01\x12\x07\n\x02vi\x10\xaf\x01\x12\x07\n\x02vo\x10\xb0\x01\x12\x07\n\x02wa\x10\xb1\x01\x12\x07\n\x02wo\x10\xb2\x01\x12\x07\n\x02xh\x10\xb3\x01\x12\x07\n\x02yi\x10\xb4\x01\x12\x07\n\x02yo\x10\xb5\x01\x12\x07\n\x02za\x10\xb6\x01\x12\x07\n\x02zh\x10\xb7\x01\x12\x07\n\x02zu\x10\xb8\x01\"\xaa\x10\n\x06Script\x12\x12\n\x0eUNKNOWN_SCRIPT\x10\x00\x12\x08\n\x04\x41\x64lm\x10\x01\x12\x08\n\x04\x41\x66\x61k\x10\x02\x12\x08\n\x04\x41ghb\x10\x03\x12\x08\n\x04\x41hom\x10\x04\x12\x08\n\x04\x41rab\x10\x05\x12\x08\n\x04\x41ran\x10\x06\x12\x08\n\x04\x41rmi\x10\x07\x12\x08\n\x04\x41rmn\x10\x08\x12\x08\n\x04\x41vst\x10\t\x12\x08\n\x04\x42\x61li\x10\n\x12\x08\n\x04\x42\x61mu\x10\x0b\x12\x08\n\x04\x42\x61ss\x10\x0c\x12\x08\n\x04\x42\x61tk\x10\r\x12\x08\n\x04\x42\x65ng\x10\x0e\x12\x08\n\x04\x42hks\x10\x0f\x12\x08\n\x04\x42lis\x10\x10\x12\x08\n\x04\x42opo\x10\x11\x12\x08\n\x04\x42rah\x10\x12\x12\x08\n\x04\x42rai\x10\x13\x12\x08\n\x04\x42ugi\x10\x14\x12\x08\n\x04\x42uhd\x10\x15\x12\x08\n\x04\x43\x61km\x10\x16\x12\x08\n\x04\x43\x61ns\x10\x17\x12\x08\n\x04\x43\x61ri\x10\x18\x12\x08\n\x04\x43ham\x10\x19\x12\x08\n\x04\x43her\x10\x1a\x12\x08\n\x04\x43irt\x10\x1b\x12\x08\n\x04\x43opt\x10\x1c\x12\x08\n\x04\x43pmn\x10\x1d\x12\x08\n\x04\x43prt\x10\x1e\x12\x08\n\x04\x43yrl\x10\x1f\x12\x08\n\x04\x43yrs\x10 \x12\x08\n\x04\x44\x65va\x10!\x12\x08\n\x04\x44ogr\x10\"\x12\x08\n\x04\x44srt\x10#\x12\x08\n\x04\x44upl\x10$\x12\x08\n\x04\x45gyd\x10%\x12\x08\n\x04\x45gyh\x10&\x12\x08\n\x04\x45gyp\x10\'\x12\x08\n\x04\x45lba\x10(\x12\x08\n\x04\x45lym\x10)\x12\x08\n\x04\x45thi\x10*\x12\x08\n\x04Geok\x10+\x12\x08\n\x04Geor\x10,\x12\x08\n\x04Glag\x10-\x12\x08\n\x04Gong\x10.\x12\x08\n\x04Gonm\x10/\x12\x08\n\x04Goth\x10\x30\x12\x08\n\x04Gran\x10\x31\x12\x08\n\x04Grek\x10\x32\x12\x08\n\x04Gujr\x10\x33\x12\x08\n\x04Guru\x10\x34\x12\x08\n\x04Hanb\x10\x35\x12\x08\n\x04Hang\x10\x36\x12\x08\n\x04Hani\x10\x37\x12\x08\n\x04Hano\x10\x38\x12\x08\n\x04Hans\x10\x39\x12\x08\n\x04Hant\x10:\x12\x08\n\x04Hatr\x10;\x12\x08\n\x04Hebr\x10<\x12\x08\n\x04Hira\x10=\x12\x08\n\x04Hluw\x10>\x12\x08\n\x04Hmng\x10?\x12\x08\n\x04Hmnp\x10@\x12\x08\n\x04Hrkt\x10\x41\x12\x08\n\x04Hung\x10\x42\x12\x08\n\x04Inds\x10\x43\x12\x08\n\x04Ital\x10\x44\x12\x08\n\x04Jamo\x10\x45\x12\x08\n\x04Java\x10\x46\x12\x08\n\x04Jpan\x10G\x12\x08\n\x04Jurc\x10H\x12\x08\n\x04Kali\x10I\x12\x08\n\x04Kana\x10J\x12\x08\n\x04Khar\x10K\x12\x08\n\x04Khmr\x10L\x12\x08\n\x04Khoj\x10M\x12\x08\n\x04Kitl\x10N\x12\x08\n\x04Kits\x10O\x12\x08\n\x04Knda\x10P\x12\x08\n\x04Kore\x10Q\x12\x08\n\x04Kpel\x10R\x12\x08\n\x04Kthi\x10S\x12\x08\n\x04Lana\x10T\x12\x08\n\x04Laoo\x10U\x12\x08\n\x04Latf\x10V\x12\x08\n\x04Latg\x10W\x12\x08\n\x04Latn\x10X\x12\x08\n\x04Leke\x10Y\x12\x08\n\x04Lepc\x10Z\x12\x08\n\x04Limb\x10[\x12\x08\n\x04Lina\x10\\\x12\x08\n\x04Linb\x10]\x12\x08\n\x04Lisu\x10^\x12\x08\n\x04Loma\x10_\x12\x08\n\x04Lyci\x10`\x12\x08\n\x04Lydi\x10\x61\x12\x08\n\x04Mahj\x10\x62\x12\x08\n\x04Maka\x10\x63\x12\x08\n\x04Mand\x10\x64\x12\x08\n\x04Mani\x10\x65\x12\x08\n\x04Marc\x10\x66\x12\x08\n\x04Maya\x10g\x12\x08\n\x04Medf\x10h\x12\x08\n\x04Mend\x10i\x12\x08\n\x04Merc\x10j\x12\x08\n\x04Mero\x10k\x12\x08\n\x04Mlym\x10l\x12\x08\n\x04Modi\x10m\x12\x08\n\x04Mong\x10n\x12\x08\n\x04Moon\x10o\x12\x08\n\x04Mroo\x10p\x12\x08\n\x04Mtei\x10q\x12\x08\n\x04Mult\x10r\x12\x08\n\x04Mymr\x10s\x12\x08\n\x04Nand\x10t\x12\x08\n\x04Narb\x10u\x12\x08\n\x04Nbat\x10v\x12\x08\n\x04Newa\x10w\x12\x08\n\x04Nkdb\x10x\x12\x08\n\x04Nkgb\x10y\x12\x08\n\x04Nkoo\x10z\x12\x08\n\x04Nshu\x10{\x12\x08\n\x04Ogam\x10|\x12\x08\n\x04Olck\x10}\x12\x08\n\x04Orkh\x10~\x12\x08\n\x04Orya\x10\x7f\x12\t\n\x04Osge\x10\x80\x01\x12\t\n\x04Osma\x10\x81\x01\x12\t\n\x04Palm\x10\x82\x01\x12\t\n\x04Pauc\x10\x83\x01\x12\t\n\x04Perm\x10\x84\x01\x12\t\n\x04Phag\x10\x85\x01\x12\t\n\x04Phli\x10\x86\x01\x12\t\n\x04Phlp\x10\x87\x01\x12\t\n\x04Phlv\x10\x88\x01\x12\t\n\x04Phnx\x10\x89\x01\x12\t\n\x04Plrd\x10\x8a\x01\x12\t\n\x04Piqd\x10\x8b\x01\x12\t\n\x04Prti\x10\x8c\x01\x12\t\n\x04Qaaa\x10\x8d\x01\x12\t\n\x04Qabx\x10\x8e\x01\x12\t\n\x04Rjng\x10\x8f\x01\x12\t\n\x04Rohg\x10\x90\x01\x12\t\n\x04Roro\x10\x91\x01\x12\t\n\x04Runr\x10\x92\x01\x12\t\n\x04Samr\x10\x93\x01\x12\t\n\x04Sara\x10\x94\x01\x12\t\n\x04Sarb\x10\x95\x01\x12\t\n\x04Saur\x10\x96\x01\x12\t\n\x04Sgnw\x10\x97\x01\x12\t\n\x04Shaw\x10\x98\x01\x12\t\n\x04Shrd\x10\x99\x01\x12\t\n\x04Shui\x10\x9a\x01\x12\t\n\x04Sidd\x10\x9b\x01\x12\t\n\x04Sind\x10\x9c\x01\x12\t\n\x04Sinh\x10\x9d\x01\x12\t\n\x04Sogd\x10\x9e\x01\x12\t\n\x04Sogo\x10\x9f\x01\x12\t\n\x04Sora\x10\xa0\x01\x12\t\n\x04Soyo\x10\xa1\x01\x12\t\n\x04Sund\x10\xa2\x01\x12\t\n\x04Sylo\x10\xa3\x01\x12\t\n\x04Syrc\x10\xa4\x01\x12\t\n\x04Syre\x10\xa5\x01\x12\t\n\x04Syrj\x10\xa6\x01\x12\t\n\x04Syrn\x10\xa7\x01\x12\t\n\x04Tagb\x10\xa8\x01\x12\t\n\x04Takr\x10\xa9\x01\x12\t\n\x04Tale\x10\xaa\x01\x12\t\n\x04Talu\x10\xab\x01\x12\t\n\x04Taml\x10\xac\x01\x12\t\n\x04Tang\x10\xad\x01\x12\t\n\x04Tavt\x10\xae\x01\x12\t\n\x04Telu\x10\xaf\x01\x12\t\n\x04Teng\x10\xb0\x01\x12\t\n\x04Tfng\x10\xb1\x01\x12\t\n\x04Tglg\x10\xb2\x01\x12\t\n\x04Thaa\x10\xb3\x01\x12\t\n\x04Thai\x10\xb4\x01\x12\t\n\x04Tibt\x10\xb5\x01\x12\t\n\x04Tirh\x10\xb6\x01\x12\t\n\x04Ugar\x10\xb7\x01\x12\t\n\x04Vaii\x10\xb8\x01\x12\t\n\x04Visp\x10\xb9\x01\x12\t\n\x04Wara\x10\xba\x01\x12\t\n\x04Wcho\x10\xbb\x01\x12\t\n\x04Wole\x10\xbc\x01\x12\t\n\x04Xpeo\x10\xbd\x01\x12\t\n\x04Xsux\x10\xbe\x01\x12\t\n\x04Yiii\x10\xbf\x01\x12\t\n\x04Zanb\x10\xc0\x01\x12\t\n\x04Zinh\x10\xc1\x01\x12\t\n\x04Zmth\x10\xc2\x01\x12\t\n\x04Zsye\x10\xc3\x01\x12\t\n\x04Zsym\x10\xc4\x01\x12\t\n\x04Zxxx\x10\xc5\x01\x12\t\n\x04Zyyy\x10\xc6\x01\x12\t\n\x04Zzzz\x10\xc7\x01\"\xe4\x11\n\x08Location\x12%\n\x07\x63ountry\x18\x01 \x01(\x0e\x32\x14.pb.Location.Country\x12\r\n\x05state\x18\x02 \x01(\t\x12\x0c\n\x04\x63ity\x18\x03 \x01(\t\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\x12\x10\n\x08latitude\x18\x05 \x01(\x11\x12\x11\n\tlongitude\x18\x06 \x01(\x11\"\xe0\x10\n\x07\x43ountry\x12\x13\n\x0fUNKNOWN_COUNTRY\x10\x00\x12\x06\n\x02\x41\x46\x10\x01\x12\x06\n\x02\x41X\x10\x02\x12\x06\n\x02\x41L\x10\x03\x12\x06\n\x02\x44Z\x10\x04\x12\x06\n\x02\x41S\x10\x05\x12\x06\n\x02\x41\x44\x10\x06\x12\x06\n\x02\x41O\x10\x07\x12\x06\n\x02\x41I\x10\x08\x12\x06\n\x02\x41Q\x10\t\x12\x06\n\x02\x41G\x10\n\x12\x06\n\x02\x41R\x10\x0b\x12\x06\n\x02\x41M\x10\x0c\x12\x06\n\x02\x41W\x10\r\x12\x06\n\x02\x41U\x10\x0e\x12\x06\n\x02\x41T\x10\x0f\x12\x06\n\x02\x41Z\x10\x10\x12\x06\n\x02\x42S\x10\x11\x12\x06\n\x02\x42H\x10\x12\x12\x06\n\x02\x42\x44\x10\x13\x12\x06\n\x02\x42\x42\x10\x14\x12\x06\n\x02\x42Y\x10\x15\x12\x06\n\x02\x42\x45\x10\x16\x12\x06\n\x02\x42Z\x10\x17\x12\x06\n\x02\x42J\x10\x18\x12\x06\n\x02\x42M\x10\x19\x12\x06\n\x02\x42T\x10\x1a\x12\x06\n\x02\x42O\x10\x1b\x12\x06\n\x02\x42Q\x10\x1c\x12\x06\n\x02\x42\x41\x10\x1d\x12\x06\n\x02\x42W\x10\x1e\x12\x06\n\x02\x42V\x10\x1f\x12\x06\n\x02\x42R\x10 \x12\x06\n\x02IO\x10!\x12\x06\n\x02\x42N\x10\"\x12\x06\n\x02\x42G\x10#\x12\x06\n\x02\x42\x46\x10$\x12\x06\n\x02\x42I\x10%\x12\x06\n\x02KH\x10&\x12\x06\n\x02\x43M\x10\'\x12\x06\n\x02\x43\x41\x10(\x12\x06\n\x02\x43V\x10)\x12\x06\n\x02KY\x10*\x12\x06\n\x02\x43\x46\x10+\x12\x06\n\x02TD\x10,\x12\x06\n\x02\x43L\x10-\x12\x06\n\x02\x43N\x10.\x12\x06\n\x02\x43X\x10/\x12\x06\n\x02\x43\x43\x10\x30\x12\x06\n\x02\x43O\x10\x31\x12\x06\n\x02KM\x10\x32\x12\x06\n\x02\x43G\x10\x33\x12\x06\n\x02\x43\x44\x10\x34\x12\x06\n\x02\x43K\x10\x35\x12\x06\n\x02\x43R\x10\x36\x12\x06\n\x02\x43I\x10\x37\x12\x06\n\x02HR\x10\x38\x12\x06\n\x02\x43U\x10\x39\x12\x06\n\x02\x43W\x10:\x12\x06\n\x02\x43Y\x10;\x12\x06\n\x02\x43Z\x10<\x12\x06\n\x02\x44K\x10=\x12\x06\n\x02\x44J\x10>\x12\x06\n\x02\x44M\x10?\x12\x06\n\x02\x44O\x10@\x12\x06\n\x02\x45\x43\x10\x41\x12\x06\n\x02\x45G\x10\x42\x12\x06\n\x02SV\x10\x43\x12\x06\n\x02GQ\x10\x44\x12\x06\n\x02\x45R\x10\x45\x12\x06\n\x02\x45\x45\x10\x46\x12\x06\n\x02\x45T\x10G\x12\x06\n\x02\x46K\x10H\x12\x06\n\x02\x46O\x10I\x12\x06\n\x02\x46J\x10J\x12\x06\n\x02\x46I\x10K\x12\x06\n\x02\x46R\x10L\x12\x06\n\x02GF\x10M\x12\x06\n\x02PF\x10N\x12\x06\n\x02TF\x10O\x12\x06\n\x02GA\x10P\x12\x06\n\x02GM\x10Q\x12\x06\n\x02GE\x10R\x12\x06\n\x02\x44\x45\x10S\x12\x06\n\x02GH\x10T\x12\x06\n\x02GI\x10U\x12\x06\n\x02GR\x10V\x12\x06\n\x02GL\x10W\x12\x06\n\x02GD\x10X\x12\x06\n\x02GP\x10Y\x12\x06\n\x02GU\x10Z\x12\x06\n\x02GT\x10[\x12\x06\n\x02GG\x10\\\x12\x06\n\x02GN\x10]\x12\x06\n\x02GW\x10^\x12\x06\n\x02GY\x10_\x12\x06\n\x02HT\x10`\x12\x06\n\x02HM\x10\x61\x12\x06\n\x02VA\x10\x62\x12\x06\n\x02HN\x10\x63\x12\x06\n\x02HK\x10\x64\x12\x06\n\x02HU\x10\x65\x12\x06\n\x02IS\x10\x66\x12\x06\n\x02IN\x10g\x12\x06\n\x02ID\x10h\x12\x06\n\x02IR\x10i\x12\x06\n\x02IQ\x10j\x12\x06\n\x02IE\x10k\x12\x06\n\x02IM\x10l\x12\x06\n\x02IL\x10m\x12\x06\n\x02IT\x10n\x12\x06\n\x02JM\x10o\x12\x06\n\x02JP\x10p\x12\x06\n\x02JE\x10q\x12\x06\n\x02JO\x10r\x12\x06\n\x02KZ\x10s\x12\x06\n\x02KE\x10t\x12\x06\n\x02KI\x10u\x12\x06\n\x02KP\x10v\x12\x06\n\x02KR\x10w\x12\x06\n\x02KW\x10x\x12\x06\n\x02KG\x10y\x12\x06\n\x02LA\x10z\x12\x06\n\x02LV\x10{\x12\x06\n\x02LB\x10|\x12\x06\n\x02LS\x10}\x12\x06\n\x02LR\x10~\x12\x06\n\x02LY\x10\x7f\x12\x07\n\x02LI\x10\x80\x01\x12\x07\n\x02LT\x10\x81\x01\x12\x07\n\x02LU\x10\x82\x01\x12\x07\n\x02MO\x10\x83\x01\x12\x07\n\x02MK\x10\x84\x01\x12\x07\n\x02MG\x10\x85\x01\x12\x07\n\x02MW\x10\x86\x01\x12\x07\n\x02MY\x10\x87\x01\x12\x07\n\x02MV\x10\x88\x01\x12\x07\n\x02ML\x10\x89\x01\x12\x07\n\x02MT\x10\x8a\x01\x12\x07\n\x02MH\x10\x8b\x01\x12\x07\n\x02MQ\x10\x8c\x01\x12\x07\n\x02MR\x10\x8d\x01\x12\x07\n\x02MU\x10\x8e\x01\x12\x07\n\x02YT\x10\x8f\x01\x12\x07\n\x02MX\x10\x90\x01\x12\x07\n\x02\x46M\x10\x91\x01\x12\x07\n\x02MD\x10\x92\x01\x12\x07\n\x02MC\x10\x93\x01\x12\x07\n\x02MN\x10\x94\x01\x12\x07\n\x02ME\x10\x95\x01\x12\x07\n\x02MS\x10\x96\x01\x12\x07\n\x02MA\x10\x97\x01\x12\x07\n\x02MZ\x10\x98\x01\x12\x07\n\x02MM\x10\x99\x01\x12\x07\n\x02NA\x10\x9a\x01\x12\x07\n\x02NR\x10\x9b\x01\x12\x07\n\x02NP\x10\x9c\x01\x12\x07\n\x02NL\x10\x9d\x01\x12\x07\n\x02NC\x10\x9e\x01\x12\x07\n\x02NZ\x10\x9f\x01\x12\x07\n\x02NI\x10\xa0\x01\x12\x07\n\x02NE\x10\xa1\x01\x12\x07\n\x02NG\x10\xa2\x01\x12\x07\n\x02NU\x10\xa3\x01\x12\x07\n\x02NF\x10\xa4\x01\x12\x07\n\x02MP\x10\xa5\x01\x12\x07\n\x02NO\x10\xa6\x01\x12\x07\n\x02OM\x10\xa7\x01\x12\x07\n\x02PK\x10\xa8\x01\x12\x07\n\x02PW\x10\xa9\x01\x12\x07\n\x02PS\x10\xaa\x01\x12\x07\n\x02PA\x10\xab\x01\x12\x07\n\x02PG\x10\xac\x01\x12\x07\n\x02PY\x10\xad\x01\x12\x07\n\x02PE\x10\xae\x01\x12\x07\n\x02PH\x10\xaf\x01\x12\x07\n\x02PN\x10\xb0\x01\x12\x07\n\x02PL\x10\xb1\x01\x12\x07\n\x02PT\x10\xb2\x01\x12\x07\n\x02PR\x10\xb3\x01\x12\x07\n\x02QA\x10\xb4\x01\x12\x07\n\x02RE\x10\xb5\x01\x12\x07\n\x02RO\x10\xb6\x01\x12\x07\n\x02RU\x10\xb7\x01\x12\x07\n\x02RW\x10\xb8\x01\x12\x07\n\x02\x42L\x10\xb9\x01\x12\x07\n\x02SH\x10\xba\x01\x12\x07\n\x02KN\x10\xbb\x01\x12\x07\n\x02LC\x10\xbc\x01\x12\x07\n\x02MF\x10\xbd\x01\x12\x07\n\x02PM\x10\xbe\x01\x12\x07\n\x02VC\x10\xbf\x01\x12\x07\n\x02WS\x10\xc0\x01\x12\x07\n\x02SM\x10\xc1\x01\x12\x07\n\x02ST\x10\xc2\x01\x12\x07\n\x02SA\x10\xc3\x01\x12\x07\n\x02SN\x10\xc4\x01\x12\x07\n\x02RS\x10\xc5\x01\x12\x07\n\x02SC\x10\xc6\x01\x12\x07\n\x02SL\x10\xc7\x01\x12\x07\n\x02SG\x10\xc8\x01\x12\x07\n\x02SX\x10\xc9\x01\x12\x07\n\x02SK\x10\xca\x01\x12\x07\n\x02SI\x10\xcb\x01\x12\x07\n\x02SB\x10\xcc\x01\x12\x07\n\x02SO\x10\xcd\x01\x12\x07\n\x02ZA\x10\xce\x01\x12\x07\n\x02GS\x10\xcf\x01\x12\x07\n\x02SS\x10\xd0\x01\x12\x07\n\x02\x45S\x10\xd1\x01\x12\x07\n\x02LK\x10\xd2\x01\x12\x07\n\x02SD\x10\xd3\x01\x12\x07\n\x02SR\x10\xd4\x01\x12\x07\n\x02SJ\x10\xd5\x01\x12\x07\n\x02SZ\x10\xd6\x01\x12\x07\n\x02SE\x10\xd7\x01\x12\x07\n\x02\x43H\x10\xd8\x01\x12\x07\n\x02SY\x10\xd9\x01\x12\x07\n\x02TW\x10\xda\x01\x12\x07\n\x02TJ\x10\xdb\x01\x12\x07\n\x02TZ\x10\xdc\x01\x12\x07\n\x02TH\x10\xdd\x01\x12\x07\n\x02TL\x10\xde\x01\x12\x07\n\x02TG\x10\xdf\x01\x12\x07\n\x02TK\x10\xe0\x01\x12\x07\n\x02TO\x10\xe1\x01\x12\x07\n\x02TT\x10\xe2\x01\x12\x07\n\x02TN\x10\xe3\x01\x12\x07\n\x02TR\x10\xe4\x01\x12\x07\n\x02TM\x10\xe5\x01\x12\x07\n\x02TC\x10\xe6\x01\x12\x07\n\x02TV\x10\xe7\x01\x12\x07\n\x02UG\x10\xe8\x01\x12\x07\n\x02UA\x10\xe9\x01\x12\x07\n\x02\x41\x45\x10\xea\x01\x12\x07\n\x02GB\x10\xeb\x01\x12\x07\n\x02US\x10\xec\x01\x12\x07\n\x02UM\x10\xed\x01\x12\x07\n\x02UY\x10\xee\x01\x12\x07\n\x02UZ\x10\xef\x01\x12\x07\n\x02VU\x10\xf0\x01\x12\x07\n\x02VE\x10\xf1\x01\x12\x07\n\x02VN\x10\xf2\x01\x12\x07\n\x02VG\x10\xf3\x01\x12\x07\n\x02VI\x10\xf4\x01\x12\x07\n\x02WF\x10\xf5\x01\x12\x07\n\x02\x45H\x10\xf6\x01\x12\x07\n\x02YE\x10\xf7\x01\x12\x07\n\x02ZM\x10\xf8\x01\x12\x07\n\x02ZW\x10\xf9\x01\x62\x06proto3') ) @@ -117,8 +117,8 @@ _SOFTWARE_OS = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=1316, - serialized_end=1400, + serialized_start=1334, + serialized_end=1418, ) _sym_db.RegisterEnumDescriptor(_SOFTWARE_OS) @@ -871,8 +871,8 @@ _LANGUAGE_LANGUAGE = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=1532, - serialized_end=3093, + serialized_start=1550, + serialized_end=3111, ) _sym_db.RegisterEnumDescriptor(_LANGUAGE_LANGUAGE) @@ -1685,8 +1685,8 @@ _LANGUAGE_SCRIPT = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=3096, - serialized_end=5186, + serialized_start=3114, + serialized_end=5204, ) _sym_db.RegisterEnumDescriptor(_LANGUAGE_SCRIPT) @@ -2699,8 +2699,8 @@ _LOCATION_COUNTRY = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=5321, - serialized_end=7465, + serialized_start=5339, + serialized_end=7483, ) _sym_db.RegisterEnumDescriptor(_LOCATION_COUNTRY) @@ -2727,7 +2727,7 @@ _CLAIM = _descriptor.Descriptor( is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='claim_list', full_name='pb.Claim.claim_list', index=2, + name='collection', full_name='pb.Claim.collection', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, @@ -3200,8 +3200,15 @@ _VIDEO = _descriptor.Descriptor( is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='audio', full_name='pb.Video.audio', index=2, - number=3, type=11, cpp_type=10, label=1, + name='duration', full_name='pb.Video.duration', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='audio', full_name='pb.Video.audio', index=3, + number=15, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, @@ -3219,7 +3226,7 @@ _VIDEO = _descriptor.Descriptor( oneofs=[ ], serialized_start=1199, - serialized_end=1263, + serialized_end=1281, ) @@ -3249,8 +3256,8 @@ _AUDIO = _descriptor.Descriptor( extension_ranges=[], oneofs=[ ], - serialized_start=1265, - serialized_end=1290, + serialized_start=1283, + serialized_end=1308, ) @@ -3281,8 +3288,8 @@ _SOFTWARE = _descriptor.Descriptor( extension_ranges=[], oneofs=[ ], - serialized_start=1292, - serialized_end=1400, + serialized_start=1310, + serialized_end=1418, ) @@ -3328,8 +3335,8 @@ _LANGUAGE = _descriptor.Descriptor( extension_ranges=[], oneofs=[ ], - serialized_start=1403, - serialized_end=5186, + serialized_start=1421, + serialized_end=5204, ) @@ -3395,13 +3402,13 @@ _LOCATION = _descriptor.Descriptor( extension_ranges=[], oneofs=[ ], - serialized_start=5189, - serialized_end=7465, + serialized_start=5207, + serialized_end=7483, ) _CLAIM.fields_by_name['stream'].message_type = _STREAM _CLAIM.fields_by_name['channel'].message_type = _CHANNEL -_CLAIM.fields_by_name['claim_list'].message_type = _CLAIMLIST +_CLAIM.fields_by_name['collection'].message_type = _CLAIMLIST _CLAIM.fields_by_name['repost'].message_type = _CLAIMREFERENCE _CLAIM.fields_by_name['thumbnail'].message_type = _SOURCE _CLAIM.fields_by_name['languages'].message_type = _LANGUAGE @@ -3413,8 +3420,8 @@ _CLAIM.oneofs_by_name['type'].fields.append( _CLAIM.fields_by_name['channel']) _CLAIM.fields_by_name['channel'].containing_oneof = _CLAIM.oneofs_by_name['type'] _CLAIM.oneofs_by_name['type'].fields.append( - _CLAIM.fields_by_name['claim_list']) -_CLAIM.fields_by_name['claim_list'].containing_oneof = _CLAIM.oneofs_by_name['type'] + _CLAIM.fields_by_name['collection']) +_CLAIM.fields_by_name['collection'].containing_oneof = _CLAIM.oneofs_by_name['type'] _CLAIM.oneofs_by_name['type'].fields.append( _CLAIM.fields_by_name['repost']) _CLAIM.fields_by_name['repost'].containing_oneof = _CLAIM.oneofs_by_name['type'] diff --git a/lbrynet/stream/managed_stream.py b/lbrynet/stream/managed_stream.py index 3a675a5ac..545d86d62 100644 --- a/lbrynet/stream/managed_stream.py +++ b/lbrynet/stream/managed_stream.py @@ -125,7 +125,7 @@ class ManagedStream: def as_dict(self) -> typing.Dict: full_path = self.full_path if self.output_file_exists else None - mime_type = guess_media_type(os.path.basename(self.descriptor.suggested_file_name)) + mime_type = guess_media_type(os.path.basename(self.descriptor.suggested_file_name))[0] if self.downloader and self.downloader.written_bytes: written_bytes = self.downloader.written_bytes diff --git a/lbrynet/wallet/transaction.py b/lbrynet/wallet/transaction.py index 8e1dafbd1..5c261853f 100644 --- a/lbrynet/wallet/transaction.py +++ b/lbrynet/wallet/transaction.py @@ -122,6 +122,10 @@ class Output(BaseOutput): self.claim.signature = private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) self.script.generate() + def clear_signature(self): + self.channel = None + self.claim.clear_signature() + def generate_channel_private_key(self): private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256) self.private_key = private_key.to_pem().decode() @@ -188,15 +192,17 @@ class Transaction(BaseTransaction): @classmethod def claim_update( - cls, previous_claim: Output, amount: int, holding_address: str, + cls, previous_claim: Output, claim: Claim, amount: int, holding_address: str, funding_accounts: List[Account], change_account: Account, signing_channel: Output = None): ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) updated_claim = Output.pay_update_claim_pubkey_hash( amount, previous_claim.claim_name, previous_claim.claim_id, - previous_claim.claim, ledger.address_to_hash160(holding_address) + claim, ledger.address_to_hash160(holding_address) ) if signing_channel is not None: updated_claim.sign(signing_channel, b'placeholder txid:nout') + else: + updated_claim.clear_signature() return cls.create( [Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account, sign=False ) diff --git a/tests/integration/test_claim_commands.py b/tests/integration/test_claim_commands.py index 322c9bc34..c3819ac5f 100644 --- a/tests/integration/test_claim_commands.py +++ b/tests/integration/test_claim_commands.py @@ -1,6 +1,9 @@ +import os.path import hashlib import tempfile +import logging from binascii import unhexlify +from urllib.request import urlopen import ecdsa @@ -12,6 +15,9 @@ from lbrynet.testcase import CommandTestCase from torba.client.hash import sha256, Base58 +log = logging.getLogger(__name__) + + class ChannelCommands(CommandTestCase): async def test_create_channel_names(self): @@ -69,74 +75,55 @@ class ChannelCommands(CommandTestCase): async def test_setting_channel_fields(self): values = { - 'tags': ["cool", "awesome"], 'title': "Cool Channel", 'description': "Best channel on LBRY.", 'thumbnail_url': "https://co.ol/thumbnail.png", + 'tags': ["cool", "awesome"], 'languages': ["en-US"], 'locations': ['US::Manchester'], 'email': "human@email.com", 'website_url': "https://co.ol", 'cover_url': "https://co.ol/cover.png", + 'featured': ['cafe'] } fixed_values = values.copy() - fixed_values['languages'] = ['en-US'] - fixed_values['locations'] = [{'country': 'US', 'city': 'Manchester'}] fixed_values['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')} + fixed_values['locations'] = [{'country': 'US', 'city': 'Manchester'}] fixed_values['cover'] = {'url': fixed_values.pop('cover_url')} # create new channel with all fields set tx = await self.out(self.channel_create('@bigchannel', **values)) - txo = tx['outputs'][0] - self.assertEqual( - txo['value'], - {'public_key': txo['value']['public_key'], **fixed_values} - ) + channel = tx['outputs'][0]['value'] + self.assertEqual(channel, {'public_key': channel['public_key'], **fixed_values}) # create channel with nothing set tx = await self.out(self.channel_create('@lightchannel')) - txo = tx['outputs'][0] - self.assertEqual( - txo['value'], - {'public_key': txo['value']['public_key']} - ) + channel = tx['outputs'][0]['value'] + self.assertEqual(channel, {'public_key': channel['public_key']}) - # create channel with just some tags - tx = await self.out(self.channel_create('@updatedchannel', tags='blah')) + # create channel with just a featured claim + tx = await self.out(self.channel_create('@featurechannel', featured='beef')) txo = tx['outputs'][0] - claim_id = txo['claim_id'] - public_key = txo['value']['public_key'] - self.assertEqual( - txo['value'], - {'public_key': public_key, 'tags': ['blah']} - ) + claim_id, channel = txo['claim_id'], txo['value'] + fixed_values['public_key'] = channel['public_key'] + self.assertEqual(channel, {'public_key': fixed_values['public_key'], 'featured': ['beef']}) # update channel setting all fields tx = await self.out(self.channel_update(claim_id, **values)) - txo = tx['outputs'][0] - fixed_values['public_key'] = public_key - fixed_values['tags'].insert(0, 'blah') # existing tag - self.assertEqual( - txo['value'], - fixed_values - ) + channel = tx['outputs'][0]['value'] + fixed_values['featured'].insert(0, 'beef') # existing featured claim + self.assertEqual(channel, fixed_values) - # clearing and settings tags - tx = await self.out(self.channel_update(claim_id, tags='single', clear_tags=True)) - txo = tx['outputs'][0] - fixed_values['tags'] = ['single'] - self.assertEqual( - txo['value'], - fixed_values - ) + # clearing and settings featured content + tx = await self.out(self.channel_update(claim_id, featured='beefcafe', clear_featured=True)) + channel = tx['outputs'][0]['value'] + fixed_values['featured'] = ['beefcafe'] + self.assertEqual(channel, fixed_values) # reset signing key tx = await self.out(self.channel_update(claim_id, new_signing_key=True)) - txo = tx['outputs'][0] - self.assertNotEqual( - txo['value']['public_key'], - fixed_values['public_key'] - ) + channel = tx['outputs'][0]['value'] + self.assertNotEqual(channel['public_key'], fixed_values['public_key']) # send channel to someone else new_account = await self.out(self.daemon.jsonrpc_account_create('second account')) @@ -168,6 +155,19 @@ class ChannelCommands(CommandTestCase): class StreamCommands(CommandTestCase): + files_directory = os.path.join(os.path.dirname(__file__), 'files') + video_file_url = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4' + video_file_name = os.path.join(files_directory, 'ForBiggerEscapes.mp4') + + def setUp(self): + if not os.path.exists(self.video_file_name): + if not os.path.exists(self.files_directory): + os.mkdir(self.files_directory) + log.info(f'downloading test video from {self.video_file_name}') + with urlopen(self.video_file_url) as response,\ + open(self.video_file_name, 'wb') as video_file: + video_file.write(response.read()) + async def test_create_stream_names(self): # claim new name await self.stream_create('foo') @@ -207,17 +207,17 @@ class StreamCommands(CommandTestCase): tx = await self.stream_update(claim_id, bid='3.0') self.assertEqual(tx['outputs'][0]['amount'], '3.0') - await self.assertBalance(self.account, '6.993384') + await self.assertBalance(self.account, '6.993337') # not enough funds with self.assertRaisesRegex( InsufficientFundsError, "Not enough funds to cover this transaction."): await self.stream_create('foo2', '9.0') self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1) - await self.assertBalance(self.account, '6.993384') + await self.assertBalance(self.account, '6.993337') # spend exactly amount available, no change - tx = await self.stream_create('foo3', '6.98527700') + tx = await self.stream_create('foo3', '6.98523') await self.assertBalance(self.account, '0.0') self.assertEqual(len(tx['outputs']), 1) # no change self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2) @@ -264,12 +264,12 @@ class StreamCommands(CommandTestCase): async def test_setting_stream_fields(self): values = { - 'tags': ["cool", "awesome"], 'title': "Cool Content", 'description': "Best content on LBRY.", 'thumbnail_url': "https://co.ol/thumbnail.png", + 'tags': ["cool", "awesome"], 'languages': ["en"], - 'locations': ['{"country": "UA"}'], + 'locations': ['{"country": "US"}'], 'author': "Jules Verne", 'license': 'Public Domain', @@ -279,16 +279,13 @@ class StreamCommands(CommandTestCase): 'fee_currency': 'usd', 'fee_amount': '2.99', 'fee_address': 'mmCsWAiXMUVecFQ3fVzUwvpT9XFMXno2Ca', - - 'video_width': 800, - 'video_height': 600 } fixed_values = values.copy() - fixed_values['languages'] = ['en'] - fixed_values['locations'] = [{'country': 'UA'}] + fixed_values['locations'] = [{'country': 'US'}] fixed_values['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')} fixed_values['release_time'] = str(values['release_time']) fixed_values['source'] = { + 'hash': 'c0ddd62c7717180e7ffb8a15bb9674d3ec92592e0b7ac7d1d5289836b4553be2', 'media_type': 'application/octet-stream', 'size': '3' } @@ -297,57 +294,68 @@ class StreamCommands(CommandTestCase): 'amount': float(fixed_values.pop('fee_amount')), 'currency': fixed_values.pop('fee_currency').upper() } - fixed_values['video'] = { - 'height': fixed_values.pop('video_height'), - 'width': fixed_values.pop('video_width') - } - # create new channel with all fields set + # create new stream with all fields set tx = await self.out(self.stream_create('big', **values)) - txo = tx['outputs'][0] - stream = txo['value'] + stream = tx['outputs'][0]['value'] + fixed_values['source']['name'] = stream['source']['name'] fixed_values['source']['sd_hash'] = stream['source']['sd_hash'] self.assertEqual(stream, fixed_values) - # create channel with nothing set + # create stream with nothing set tx = await self.out(self.stream_create('light')) - txo = tx['outputs'][0] + stream = tx['outputs'][0]['value'] self.assertEqual( - txo['value'], { + stream, { 'source': { 'size': '3', 'media_type': 'application/octet-stream', - 'sd_hash': txo['value']['source']['sd_hash'] + 'name': stream['source']['name'], + 'hash': 'c0ddd62c7717180e7ffb8a15bb9674d3ec92592e0b7ac7d1d5289836b4553be2', + 'sd_hash': stream['source']['sd_hash'] }, } ) - # create channel with just some tags - tx = await self.out(self.stream_create('updated', tags='blah')) + # create stream with just some tags, langs and locations + tx = await self.out(self.stream_create('updated', tags='blah', languages='uk', locations='UA::Kyiv')) txo = tx['outputs'][0] - claim_id = txo['claim_id'] - fixed_values['source']['sd_hash'] = txo['value']['source']['sd_hash'] + claim_id, stream = txo['claim_id'], txo['value'] + fixed_values['source']['name'] = stream['source']['name'] + fixed_values['source']['sd_hash'] = stream['source']['sd_hash'] self.assertEqual( - txo['value'], { + stream, { 'source': { 'size': '3', 'media_type': 'application/octet-stream', + 'name': fixed_values['source']['name'], + 'hash': 'c0ddd62c7717180e7ffb8a15bb9674d3ec92592e0b7ac7d1d5289836b4553be2', 'sd_hash': fixed_values['source']['sd_hash'], }, - 'tags': ['blah'] + 'tags': ['blah'], + 'languages': ['uk'], + 'locations': [{'country': 'UA', 'city': 'Kyiv'}] } ) - # update channel setting all fields + # update stream setting all fields, 'source' doesn't change tx = await self.out(self.stream_update(claim_id, **values)) - txo = tx['outputs'][0] + stream = tx['outputs'][0]['value'] fixed_values['tags'].insert(0, 'blah') # existing tag - self.assertEqual(txo['value'], fixed_values) + fixed_values['languages'].insert(0, 'uk') # existing language + fixed_values['locations'].insert(0, {'country': 'UA', 'city': 'Kyiv'}) # existing location + self.assertEqual(stream, fixed_values) # clearing and settings tags - tx = await self.out(self.stream_update(claim_id, tags='single', clear_tags=True)) + tx = await self.out(self.stream_update( + claim_id, tags='single', clear_tags=True, + languages='pt', clear_languages=True, + locations='BR', clear_locations=True, + )) txo = tx['outputs'][0] fixed_values['tags'] = ['single'] + fixed_values['languages'] = ['pt'] + fixed_values['locations'] = [{'country': 'BR'}] self.assertEqual(txo['value'], fixed_values) # send claim to someone else @@ -365,7 +373,55 @@ class StreamCommands(CommandTestCase): self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2) self.assertEqual(len(await self.daemon.jsonrpc_claim_list(account_id=account2_id)), 1) - async def test_create_update_and_abandon_claim(self): + async def test_automatic_type_and_metadata_detection(self): + tx = await self.out( + self.daemon.jsonrpc_stream_create( + 'chrome', '1.0', file_path=self.video_file_name + ) + ) + txo = tx['outputs'][0] + self.assertEqual( + txo['value'], { + 'source': { + 'size': '2299653', + 'name': 'ForBiggerEscapes.mp4', + 'media_type': 'video/mp4', + 'hash': 'f846d9c7f5ed28f0ed47e9d9b4198a03075e6df967ac54078af85ea1bf0ddd87', + 'sd_hash': txo['value']['source']['sd_hash'], + }, + 'video': { + 'width': 1280, + 'height': 720, + 'duration': 15 + } + } + ) + + async def test_overriding_automatic_metadata_detection(self): + tx = await self.out( + self.daemon.jsonrpc_stream_create( + 'chrome', '1.0', file_path=self.video_file_name, width=99, height=88, duration=9 + ) + ) + txo = tx['outputs'][0] + self.assertEqual( + txo['value'], { + 'source': { + 'size': '2299653', + 'name': 'ForBiggerEscapes.mp4', + 'media_type': 'video/mp4', + 'hash': 'f846d9c7f5ed28f0ed47e9d9b4198a03075e6df967ac54078af85ea1bf0ddd87', + 'sd_hash': txo['value']['source']['sd_hash'], + }, + 'video': { + 'width': 99, + 'height': 88, + 'duration': 9 + } + } + ) + + async def test_create_update_and_abandon_stream(self): await self.assertBalance(self.account, '10.0') tx = await self.stream_create(bid='2.5') # creates new claim @@ -385,8 +441,8 @@ class StreamCommands(CommandTestCase): self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5') self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id) self.assertEqual(txs[0]['value'], '0.0') - self.assertEqual(txs[0]['fee'], '-0.000184') - await self.assertBalance(self.account, '8.979709') + self.assertEqual(txs[0]['fee'], '-0.0002075') + await self.assertBalance(self.account, '8.9796855') await self.stream_abandon(claim_id) txs = await self.out(self.daemon.jsonrpc_transaction_list()) @@ -395,9 +451,9 @@ class StreamCommands(CommandTestCase): self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim_id) self.assertEqual(txs[0]['value'], '0.0') self.assertEqual(txs[0]['fee'], '-0.000107') - await self.assertBalance(self.account, '9.979602') + await self.assertBalance(self.account, '9.9795785') - async def test_abandoning_claim_at_loss(self): + async def test_abandoning_stream_at_loss(self): await self.assertBalance(self.account, '10.0') tx = await self.stream_create(bid='0.0001') await self.assertBalance(self.account, '9.979793') diff --git a/tests/unit/lbrynet_daemon/test_mime_types.py b/tests/unit/lbrynet_daemon/test_mime_types.py index 6d2143471..8d7d8b772 100644 --- a/tests/unit/lbrynet_daemon/test_mime_types.py +++ b/tests/unit/lbrynet_daemon/test_mime_types.py @@ -4,13 +4,13 @@ from lbrynet.schema import mime_types class TestMimeTypes(unittest.TestCase): def test_mp4_video(self): - self.assertEqual("video/mp4", mime_types.guess_media_type("test.mp4")) - self.assertEqual("video/mp4", mime_types.guess_media_type("test.MP4")) + self.assertEqual("video/mp4", mime_types.guess_media_type("test.mp4")[0]) + self.assertEqual("video/mp4", mime_types.guess_media_type("test.MP4")[0]) def test_x_ext_(self): - self.assertEqual("application/x-ext-lbry", mime_types.guess_media_type("test.lbry")) - self.assertEqual("application/x-ext-lbry", mime_types.guess_media_type("test.LBRY")) + self.assertEqual("application/x-ext-lbry", mime_types.guess_media_type("test.lbry")[0]) + self.assertEqual("application/x-ext-lbry", mime_types.guess_media_type("test.LBRY")[0]) def test_octet_stream(self): - self.assertEqual("application/octet-stream", mime_types.guess_media_type("test.")) - self.assertEqual("application/octet-stream", mime_types.guess_media_type("test")) + self.assertEqual("application/octet-stream", mime_types.guess_media_type("test.")[0]) + self.assertEqual("application/octet-stream", mime_types.guess_media_type("test")[0]) diff --git a/tests/unit/schema/test_models.py b/tests/unit/schema/test_models.py index 1955a417e..0ab8e1255 100644 --- a/tests/unit/schema/test_models.py +++ b/tests/unit/schema/test_models.py @@ -1,7 +1,7 @@ from unittest import TestCase from decimal import Decimal -from lbrynet.schema.claim import Claim, Channel, Stream +from lbrynet.schema.claim import Claim, Stream class TestClaimContainerAwareness(TestCase): @@ -9,27 +9,13 @@ class TestClaimContainerAwareness(TestCase): def test_stream_claim(self): stream = Stream() claim = stream.claim - self.assertTrue(claim.is_stream) - self.assertFalse(claim.is_channel) + self.assertEqual(claim.claim_type, Claim.STREAM) claim = Claim.from_bytes(claim.to_bytes()) - self.assertTrue(claim.is_stream) - self.assertFalse(claim.is_channel) + self.assertEqual(claim.claim_type, Claim.STREAM) self.assertIsNotNone(claim.stream) with self.assertRaisesRegex(ValueError, 'Claim is not a channel.'): print(claim.channel) - def test_channel_claim(self): - channel = Channel() - claim = channel.claim - self.assertFalse(claim.is_stream) - self.assertTrue(claim.is_channel) - claim = Claim.from_bytes(claim.to_bytes()) - self.assertFalse(claim.is_stream) - self.assertTrue(claim.is_channel) - self.assertIsNotNone(claim.channel) - with self.assertRaisesRegex(ValueError, 'Claim is not a stream.'): - print(claim.stream) - class TestFee(TestCase):