forked from LBRYCommunity/lbry-sdk
exposed new protobuf fields in APIs and updated tests
This commit is contained in:
parent
61cadb5cbe
commit
71f5061848
13 changed files with 964 additions and 824 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -9,3 +9,5 @@
|
|||
lbrynet.egg-info
|
||||
__pycache__
|
||||
_trial_temp/
|
||||
|
||||
/tests/integration/files
|
||||
|
|
|
@ -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> | --name=<name>) (<bid> | --bid=<bid>)
|
||||
[--allow_duplicate_name=<allow_duplicate_name>]
|
||||
[--title=<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,6 +3292,7 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
if 'fee_address' in kwargs:
|
||||
self.valid_address_or_error(kwargs['fee_address'])
|
||||
return kwargs['fee_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:
|
||||
|
|
|
@ -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()
|
||||
|
|
512
lbrynet/schema/attrs.py
Normal file
512
lbrynet/schema/attrs.py
Normal file
|
@ -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)
|
|
@ -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]
|
||||
|
|
|
@ -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}"
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
|
|
@ -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'
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
Loading…
Reference in a new issue