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
|
lbrynet.egg-info
|
||||||
__pycache__
|
__pycache__
|
||||||
_trial_temp/
|
_trial_temp/
|
||||||
|
|
||||||
|
/tests/integration/files
|
||||||
|
|
|
@ -1622,16 +1622,16 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
|
|
||||||
@requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED])
|
@requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED])
|
||||||
async def jsonrpc_channel_create(
|
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.
|
Create a new channel by generating a channel private key and establishing an '@' prefixed claim.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
channel_create (<name> | --name=<name>) (<bid> | --bid=<bid>)
|
channel_create (<name> | --name=<name>) (<bid> | --bid=<bid>)
|
||||||
[--allow_duplicate_name=<allow_duplicate_name>]
|
[--allow_duplicate_name=<allow_duplicate_name>]
|
||||||
[--title=<title>] [--description=<description>]
|
[--title=<title>] [--description=<description>] [--email=<email>] [--featured=<featured>...]
|
||||||
[--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]
|
[--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]
|
||||||
[--email=<email>]
|
|
||||||
[--website_url=<website_url>] [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>]
|
[--website_url=<website_url>] [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>]
|
||||||
[--account_id=<account_id>] [--claim_address=<claim_address>] [--preview]
|
[--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
|
--bid=<bid> : (decimal) amount to back the claim
|
||||||
--title=<title> : (str) title of the publication
|
--title=<title> : (str) title of the publication
|
||||||
--description=<description> : (str) description 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
|
--tags=<tags> : (list) content tags
|
||||||
--languages=<languages> : (list) languages used by the channel,
|
--languages=<languages> : (list) languages used by the channel,
|
||||||
using RFC 5646 format, eg:
|
using RFC 5646 format, eg:
|
||||||
|
@ -1737,6 +1738,7 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
Usage:
|
Usage:
|
||||||
channel_update (<claim_id> | --claim_id=<claim_id>) [<bid> | --bid=<bid>]
|
channel_update (<claim_id> | --claim_id=<claim_id>) [<bid> | --bid=<bid>]
|
||||||
[--title=<title>] [--description=<description>]
|
[--title=<title>] [--description=<description>]
|
||||||
|
[--featured=<featured>...] [--clear_featured]
|
||||||
[--tags=<tags>...] [--clear_tags]
|
[--tags=<tags>...] [--clear_tags]
|
||||||
[--languages=<languages>...] [--clear_languages]
|
[--languages=<languages>...] [--clear_languages]
|
||||||
[--locations=<locations>...] [--clear_locations]
|
[--locations=<locations>...] [--clear_locations]
|
||||||
|
@ -1749,6 +1751,8 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
--bid=<bid> : (decimal) amount to back the claim
|
--bid=<bid> : (decimal) amount to back the claim
|
||||||
--title=<title> : (str) title of the publication
|
--title=<title> : (str) title of the publication
|
||||||
--description=<description> : (str) description 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)
|
--clear_tags : (bool) clear existing tags (prior to adding new ones)
|
||||||
--tags=<tags> : (list) add content tags
|
--tags=<tags> : (list) add content tags
|
||||||
--clear_languages : (bool) clear existing languages (prior to adding new ones)
|
--clear_languages : (bool) clear existing languages (prior to adding new ones)
|
||||||
|
@ -1825,9 +1829,10 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
else:
|
else:
|
||||||
claim_address = old_txo.get_address(account.ledger)
|
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(
|
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]
|
new_txo = tx.outputs[0]
|
||||||
|
|
||||||
|
@ -1970,16 +1975,13 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
publish (<name> | --name=<name>) [--bid=<bid>] [--file_path=<file_path>]
|
publish (<name> | --name=<name>) [--bid=<bid>] [--file_path=<file_path>]
|
||||||
[<stream_type> | --stream_type=<stream_type>]
|
|
||||||
[--tags=<tags>...] [--clear_tags]
|
[--tags=<tags>...] [--clear_tags]
|
||||||
[--languages=<languages>...] [--clear_languages]
|
[--languages=<languages>...] [--clear_languages]
|
||||||
[--locations=<locations>...] [--clear_locations]
|
[--locations=<locations>...] [--clear_locations]
|
||||||
[--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]
|
[--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]
|
||||||
[--title=<title>] [--description=<description>] [--author=<author>] [--language=<language>]
|
[--title=<title>] [--description=<description>] [--author=<author>] [--language=<language>]
|
||||||
[--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
|
[--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
|
||||||
[--release_time=<release_time>]
|
[--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]
|
||||||
[--video_width=<video_width>] [--video_height=<video_height>] [--video_duration=<video_duration>]
|
|
||||||
[--image_width=<image_width>] [--image_height=<image_height>] [--audio_duration=<audio_duration>]
|
|
||||||
[--channel_id=<channel_id>] [--channel_name=<channel_name>]
|
[--channel_id=<channel_id>] [--channel_name=<channel_name>]
|
||||||
[--channel_account_id=<channel_account_id>...]
|
[--channel_account_id=<channel_account_id>...]
|
||||||
[--account_id=<account_id>] [--claim_address=<claim_address>] [--preview]
|
[--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))
|
--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
|
--bid=<bid> : (decimal) amount to back the claim
|
||||||
--file_path=<file_path> : (str) path to file to be associated with name.
|
--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_currency=<fee_currency> : (string) specify fee currency
|
||||||
--fee_amount=<fee_amount> : (decimal) content download fee
|
--fee_amount=<fee_amount> : (decimal) content download fee
|
||||||
--fee_address=<fee_address> : (str) address where to send fee payments, will use
|
--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=<license> : (str) publication license
|
||||||
--license_url=<license_url> : (str) publication license url
|
--license_url=<license_url> : (str) publication license url
|
||||||
--thumbnail_url=<thumbnail_url>: (str) thumbnail url
|
--thumbnail_url=<thumbnail_url>: (str) thumbnail url
|
||||||
--release_time=<duration> : (int) original public release of content, seconds since UNIX epoch
|
--release_time=<release_time> : (int) original public release of content, seconds since UNIX epoch
|
||||||
--duration=<duration> : (int) audio/video duration in seconds, an attempt will be made to
|
--width=<width> : (int) image/video width, automatically calculated from media file
|
||||||
calculate this automatically if not provided
|
--height=<height> : (int) image/video height, automatically calculated from media file
|
||||||
--image_width=<image_width> : (int) image width
|
--duration=<duration> : (int) audio/video duration in seconds, automatically calculated
|
||||||
--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
|
|
||||||
--channel_id=<channel_id> : (str) claim id of the publisher channel
|
--channel_id=<channel_id> : (str) claim id of the publisher channel
|
||||||
--channel_name=<channel_name> : (str) name of 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
|
--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.
|
Make a new stream claim and announce the associated file to lbrynet.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
stream_create (<name> | --name=<name>) (<bid> | --bid=<bid>)
|
stream_create (<name> | --name=<name>) (<bid> | --bid=<bid>) (<file_path> | --file_path=<file_path>)
|
||||||
(<file_path> | --file_path=<file_path>) [<stream_type> | --stream_type=<stream_type>]
|
|
||||||
[--allow_duplicate_name=<allow_duplicate_name>]
|
[--allow_duplicate_name=<allow_duplicate_name>]
|
||||||
[--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]
|
[--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]
|
||||||
[--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]
|
[--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]
|
||||||
[--title=<title>] [--description=<description>] [--author=<author>]
|
[--title=<title>] [--description=<description>] [--author=<author>]
|
||||||
[--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
|
[--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
|
||||||
[--release_time=<release_time>]
|
[--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]
|
||||||
[--video_width=<video_width>] [--video_height=<video_height>] [--video_duration=<video_duration>]
|
|
||||||
[--image_width=<image_width>] [--image_height=<image_height>] [--audio_duration=<audio_duration>]
|
|
||||||
[--channel_id=<channel_id>] [--channel_name=<channel_name>]
|
[--channel_id=<channel_id>] [--channel_name=<channel_name>]
|
||||||
[--channel_account_id=<channel_account_id>...]
|
[--channel_account_id=<channel_account_id>...]
|
||||||
[--account_id=<account_id>] [--claim_address=<claim_address>] [--preview]
|
[--account_id=<account_id>] [--claim_address=<claim_address>] [--preview]
|
||||||
|
@ -2114,7 +2105,6 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
given name. default: false.
|
given name. default: false.
|
||||||
--bid=<bid> : (decimal) amount to back the claim
|
--bid=<bid> : (decimal) amount to back the claim
|
||||||
--file_path=<file_path> : (str) path to file to be associated with name.
|
--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_currency=<fee_currency> : (string) specify fee currency
|
||||||
--fee_amount=<fee_amount> : (decimal) content download fee
|
--fee_amount=<fee_amount> : (decimal) content download fee
|
||||||
--fee_address=<fee_address> : (str) address where to send fee payments, will use
|
--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=<license> : (str) publication license
|
||||||
--license_url=<license_url> : (str) publication license url
|
--license_url=<license_url> : (str) publication license url
|
||||||
--thumbnail_url=<thumbnail_url>: (str) thumbnail url
|
--thumbnail_url=<thumbnail_url>: (str) thumbnail url
|
||||||
--release_time=<duration> : (int) original public release of content, seconds since UNIX epoch
|
--release_time=<release_time> : (int) original public release of content, seconds since UNIX epoch
|
||||||
--duration=<duration> : (int) audio/video duration in seconds, an attempt will be made to
|
--width=<width> : (int) image/video width, automatically calculated from media file
|
||||||
calculate this automatically if not provided
|
--height=<height> : (int) image/video height, automatically calculated from media file
|
||||||
--image_width=<image_width> : (int) image width
|
--duration=<duration> : (int) audio/video duration in seconds, automatically calculated
|
||||||
--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
|
|
||||||
--channel_id=<channel_id> : (str) claim id of the publisher channel
|
--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
|
--channel_account_id=<channel_id>: (str) one or more account ids for accounts to look in
|
||||||
for channel certificates, defaults to all accounts.
|
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>]
|
[--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]
|
||||||
[--title=<title>] [--description=<description>] [--author=<author>] [--language=<language>]
|
[--title=<title>] [--description=<description>] [--author=<author>] [--language=<language>]
|
||||||
[--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
|
[--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
|
||||||
[--release_time=<release_time>] [--stream_type=<stream_type>]
|
[--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]
|
||||||
[--video_width=<video_width>] [--video_height=<video_height>] [--video_duration=<video_duration>]
|
|
||||||
[--image_width=<image_width>] [--image_height=<image_height>] [--audio_duration=<audio_duration>]
|
|
||||||
[--channel_id=<channel_id>] [--channel_name=<channel_name>] [--clear_channel]
|
[--channel_id=<channel_id>] [--channel_name=<channel_name>] [--clear_channel]
|
||||||
[--channel_account_id=<channel_account_id>...]
|
[--channel_account_id=<channel_account_id>...]
|
||||||
[--account_id=<account_id>] [--claim_address=<claim_address>] [--preview]
|
[--account_id=<account_id>] [--claim_address=<claim_address>] [--preview]
|
||||||
|
@ -2316,16 +2297,10 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
--license=<license> : (str) publication license
|
--license=<license> : (str) publication license
|
||||||
--license_url=<license_url> : (str) publication license url
|
--license_url=<license_url> : (str) publication license url
|
||||||
--thumbnail_url=<thumbnail_url>: (str) thumbnail url
|
--thumbnail_url=<thumbnail_url>: (str) thumbnail url
|
||||||
--release_time=<duration> : (int) original public release of content, seconds since UNIX epoch
|
--release_time=<release_time> : (int) original public release of content, seconds since UNIX epoch
|
||||||
--stream_type=<stream_type> : (str) type of stream
|
--width=<width> : (int) image/video width, automatically calculated from media file
|
||||||
--image_width=<image_width> : (int) image width
|
--height=<height> : (int) image/video height, automatically calculated from media file
|
||||||
--image_height=<image_height> : (int) image height
|
--duration=<duration> : (int) audio/video duration in seconds, automatically calculated
|
||||||
--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
|
|
||||||
--channel_id=<channel_id> : (str) claim id of the publisher channel
|
--channel_id=<channel_id> : (str) claim id of the publisher channel
|
||||||
--clear_channel : (bool) remove channel signature
|
--clear_channel : (bool) remove channel signature
|
||||||
--channel_account_id=<channel_id>: (str) one or more account ids for accounts to look in
|
--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:
|
elif old_txo.claim.is_signed and not clear_channel:
|
||||||
channel = old_txo.channel
|
channel = old_txo.channel
|
||||||
|
|
||||||
kwargs['fee_address'] = self.get_fee_address(kwargs, claim_address)
|
if 'fee_address' in kwargs:
|
||||||
old_txo.claim.stream.update(**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(
|
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]
|
new_txo = tx.outputs[0]
|
||||||
|
|
||||||
|
@ -3314,6 +3292,7 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
if 'fee_address' in kwargs:
|
if 'fee_address' in kwargs:
|
||||||
self.valid_address_or_error(kwargs['fee_address'])
|
self.valid_address_or_error(kwargs['fee_address'])
|
||||||
return kwargs['fee_address']
|
return kwargs['fee_address']
|
||||||
|
if 'fee_currency' in kwargs or 'fee_amount' in kwargs:
|
||||||
return claim_address
|
return claim_address
|
||||||
|
|
||||||
async def get_receiving_address(self, address: str, account: LBCAccount) -> str:
|
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:
|
if txo.script.is_claim_name or txo.script.is_update_claim:
|
||||||
output['value'] = txo.claim
|
output['value'] = txo.claim
|
||||||
if txo.claim.is_channel:
|
output['sub_type'] = txo.claim.claim_type
|
||||||
output['sub_type'] = 'channel'
|
|
||||||
elif txo.claim.is_stream:
|
|
||||||
output['sub_type'] = 'stream'
|
|
||||||
if txo.channel is not None:
|
if txo.channel is not None:
|
||||||
output['signing_channel'] = {
|
output['signing_channel'] = {
|
||||||
'name': txo.channel.claim_name,
|
'name': txo.channel.claim_name,
|
||||||
|
@ -210,8 +207,4 @@ class JSONResponseEncoder(JSONEncoder):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encode_claim(claim):
|
def encode_claim(claim):
|
||||||
if claim.is_stream:
|
return getattr(claim, claim.claim_type).to_dict()
|
||||||
return claim.stream.to_dict()
|
|
||||||
elif claim.is_channel:
|
|
||||||
return claim.channel.to_dict()
|
|
||||||
return claim.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 binascii import hexlify, unhexlify
|
||||||
|
from typing import List, Iterator, TypeVar, Generic
|
||||||
|
|
||||||
from google.protobuf.message import DecodeError
|
from google.protobuf.message import DecodeError
|
||||||
from google.protobuf.json_format import MessageToDict
|
from google.protobuf.json_format import MessageToDict
|
||||||
|
@ -21,9 +22,10 @@ class Signable:
|
||||||
self.unsigned_payload = None
|
self.unsigned_payload = None
|
||||||
self.signing_channel_hash = None
|
self.signing_channel_hash = None
|
||||||
|
|
||||||
@property
|
def clear_signature(self):
|
||||||
def is_undetermined(self):
|
self.signature = None
|
||||||
return self.message.WhichOneof('type') is None
|
self.unsigned_payload = None
|
||||||
|
self.signing_channel_hash = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def signing_channel_id(self):
|
def signing_channel_id(self):
|
||||||
|
@ -72,3 +74,48 @@ class Signable:
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self):
|
||||||
return self.to_bytes()
|
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 logging
|
||||||
import json
|
from typing import List
|
||||||
from string import ascii_letters
|
|
||||||
from typing import List, Tuple, Iterator, TypeVar, Generic
|
|
||||||
from decimal import Decimal, ROUND_UP
|
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
|
|
||||||
from google.protobuf.json_format import MessageToDict
|
from google.protobuf.json_format import MessageToDict
|
||||||
from google.protobuf.message import DecodeError
|
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.parser import createParser as binary_file_parser
|
||||||
from hachoir.metadata import extractMetadata as binary_file_metadata
|
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 import compat
|
||||||
from lbrynet.schema.base import Signable
|
from lbrynet.schema.base import Signable
|
||||||
from lbrynet.schema.mime_types import guess_media_type
|
from lbrynet.schema.mime_types import guess_media_type, guess_stream_type
|
||||||
from lbrynet.schema.types.v2.claim_pb2 import (
|
from lbrynet.schema.attrs import (
|
||||||
Claim as ClaimMessage,
|
Source, Playable, Dimmensional, Fee, Image, Video, Audio,
|
||||||
Fee as FeeMessage,
|
LanguageList, LocationList, ClaimList, ClaimReference
|
||||||
Location as LocationMessage,
|
|
||||||
Language as LanguageMessage
|
|
||||||
)
|
)
|
||||||
|
from lbrynet.schema.types.v2.claim_pb2 import Claim as ClaimMessage
|
||||||
|
|
||||||
|
|
||||||
hachoir_log.use_print = False
|
hachoir_log.use_print = False
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Claim(Signable):
|
class Claim(Signable):
|
||||||
|
|
||||||
|
STREAM = 'stream'
|
||||||
|
CHANNEL = 'channel'
|
||||||
|
COLLECTION = 'collection'
|
||||||
|
REPOST = 'repost'
|
||||||
|
|
||||||
__slots__ = 'version',
|
__slots__ = 'version',
|
||||||
|
|
||||||
message_class = ClaimMessage
|
message_class = ClaimMessage
|
||||||
|
|
||||||
def __init__(self, claim_message=None):
|
def __init__(self, message=None):
|
||||||
super().__init__(claim_message)
|
super().__init__(message)
|
||||||
self.version = 2
|
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
|
@property
|
||||||
def is_stream(self):
|
def is_stream(self):
|
||||||
return self.message.WhichOneof('type') == 'stream'
|
return self.claim_type == self.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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stream(self) -> 'Stream':
|
def stream(self) -> 'Stream':
|
||||||
return Stream(self)
|
return Stream(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channel_message(self):
|
def is_channel(self):
|
||||||
if self.is_undetermined:
|
return self.claim_type == self.CHANNEL
|
||||||
self.message.channel.SetInParent()
|
|
||||||
if not self.is_channel:
|
|
||||||
raise ValueError('Claim is not a channel.')
|
|
||||||
return self.message.channel
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channel(self) -> 'Channel':
|
def channel(self) -> 'Channel':
|
||||||
return Channel(self)
|
return Channel(self)
|
||||||
|
|
||||||
def to_dict(self):
|
@property
|
||||||
return MessageToDict(self.message, preserving_proto_field_name=True)
|
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
|
@classmethod
|
||||||
def from_bytes(cls, data: bytes) -> 'Claim':
|
def from_bytes(cls, data: bytes) -> 'Claim':
|
||||||
|
@ -89,494 +98,21 @@ class Claim(Signable):
|
||||||
return claim
|
return claim
|
||||||
|
|
||||||
|
|
||||||
I = TypeVar('I')
|
class BaseClaim:
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
__slots__ = 'claim', 'message'
|
__slots__ = 'claim', 'message'
|
||||||
|
|
||||||
|
claim_type = None
|
||||||
object_fields = 'thumbnail',
|
object_fields = 'thumbnail',
|
||||||
repeat_fields = 'tags', 'languages', 'locations'
|
repeat_fields = 'tags', 'languages', 'locations'
|
||||||
|
|
||||||
def __init__(self, claim: Claim):
|
def __init__(self, claim: Claim = None):
|
||||||
self.claim = claim or Claim()
|
self.claim = claim or Claim()
|
||||||
|
self.message = self.claim.get_message(self.claim_type)
|
||||||
|
|
||||||
def to_dict(self):
|
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:
|
if 'languages' in claim:
|
||||||
claim['languages'] = self.langtags
|
claim['languages'] = self.langtags
|
||||||
return claim
|
return claim
|
||||||
|
@ -642,78 +178,19 @@ class BaseClaimSubType:
|
||||||
return LocationList(self.claim.message.locations)
|
return LocationList(self.claim.message.locations)
|
||||||
|
|
||||||
|
|
||||||
class Channel(BaseClaimSubType):
|
class Stream(BaseClaim):
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
object_fields = BaseClaimSubType.object_fields + ('cover',)
|
claim_type = Claim.STREAM
|
||||||
repeat_fields = BaseClaimSubType.repeat_fields + ('featured',)
|
|
||||||
|
|
||||||
def __init__(self, claim: Claim = None):
|
object_fields = BaseClaim.object_fields + ('source',)
|
||||||
super().__init__(claim)
|
|
||||||
self.message = self.claim.channel_message
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
claim = super().to_dict()
|
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 'source' in claim:
|
||||||
|
if 'hash' in claim['source']:
|
||||||
|
claim['source']['hash'] = self.source.file_hash
|
||||||
if 'sd_hash' in claim['source']:
|
if 'sd_hash' in claim['source']:
|
||||||
claim['source']['sd_hash'] = self.source.sd_hash
|
claim['source']['sd_hash'] = self.source.sd_hash
|
||||||
fee = claim.get('fee', {})
|
fee = claim.get('fee', {})
|
||||||
|
@ -723,58 +200,39 @@ class Stream(BaseClaimSubType):
|
||||||
fee['amount'] = self.fee.amount
|
fee['amount'] = self.fee.amount
|
||||||
return claim
|
return claim
|
||||||
|
|
||||||
def update(
|
def update(self, file_path=None, height=None, width=None, duration=None, **kwargs):
|
||||||
self, file_path=None, stream_type=None,
|
self.fee.update(
|
||||||
fee_currency=None, fee_amount=None, fee_address=None,
|
kwargs.pop('fee_address', None),
|
||||||
**kwargs):
|
kwargs.pop('fee_currency', None),
|
||||||
|
kwargs.pop('fee_amount', None)
|
||||||
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)
|
|
||||||
|
|
||||||
if 'sd_hash' in kwargs:
|
if 'sd_hash' in kwargs:
|
||||||
self.source.sd_hash = kwargs.pop('sd_hash')
|
self.source.sd_hash = kwargs.pop('sd_hash')
|
||||||
|
|
||||||
super().update(**kwargs)
|
stream_type = None
|
||||||
|
|
||||||
if file_path is not None:
|
if file_path is not None:
|
||||||
self.source.media_type = guess_media_type(file_path)
|
stream_type = self.source.update(file_path=file_path)
|
||||||
if not os.path.isfile(file_path):
|
elif self.source.name:
|
||||||
raise Exception(f"File does not exist: {file_path}")
|
self.source.media_type, stream_type = guess_media_type(self.source.name)
|
||||||
self.source.size = os.path.getsize(file_path)
|
elif self.source.media_type:
|
||||||
if self.source.size == 0:
|
stream_type = guess_stream_type(self.source.media_type)
|
||||||
raise Exception(f"Cannot publish empty file: {file_path}")
|
|
||||||
|
|
||||||
if fee_amount and fee_currency:
|
if stream_type in ('image', 'video', 'audio'):
|
||||||
if fee_address:
|
media = getattr(self, stream_type)
|
||||||
self.fee.address = fee_address
|
media_args = {'file_metadata': None}
|
||||||
if fee_currency.lower() == 'lbc':
|
try:
|
||||||
self.fee.lbc = Decimal(fee_amount)
|
media_args['file_metadata'] = binary_file_metadata(binary_file_parser(file_path))
|
||||||
elif fee_currency.lower() == 'btc':
|
except:
|
||||||
self.fee.btc = Decimal(fee_amount)
|
log.exception('Could not read file metadata.')
|
||||||
elif fee_currency.lower() == 'usd':
|
if isinstance(media, Playable):
|
||||||
self.fee.usd = Decimal(fee_amount)
|
media_args['duration'] = duration
|
||||||
else:
|
if isinstance(media, Dimmensional):
|
||||||
raise Exception(f'Unknown currency type: {fee_currency}')
|
media_args['height'] = height
|
||||||
|
media_args['width'] = width
|
||||||
|
media.update(**media_args)
|
||||||
|
|
||||||
|
super().update(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def author(self) -> str:
|
def author(self) -> str:
|
||||||
|
@ -831,3 +289,90 @@ class Stream(BaseClaimSubType):
|
||||||
@property
|
@property
|
||||||
def audio(self) -> Audio:
|
def audio(self) -> Audio:
|
||||||
return Audio(self.message.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()
|
extension = ext.strip().lower()
|
||||||
if extension[1:]:
|
if extension[1:]:
|
||||||
if extension in types_map:
|
if extension in types_map:
|
||||||
return types_map[extension][0]
|
return types_map[extension]
|
||||||
return f'application/x-ext-{extension[1:]}'
|
return f'application/x-ext-{extension[1:]}', 'binary'
|
||||||
return 'application/octet-stream'
|
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:
|
def as_dict(self) -> typing.Dict:
|
||||||
full_path = self.full_path if self.output_file_exists else None
|
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:
|
if self.downloader and self.downloader.written_bytes:
|
||||||
written_bytes = 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.claim.signature = private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
|
||||||
self.script.generate()
|
self.script.generate()
|
||||||
|
|
||||||
|
def clear_signature(self):
|
||||||
|
self.channel = None
|
||||||
|
self.claim.clear_signature()
|
||||||
|
|
||||||
def generate_channel_private_key(self):
|
def generate_channel_private_key(self):
|
||||||
private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256)
|
private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256)
|
||||||
self.private_key = private_key.to_pem().decode()
|
self.private_key = private_key.to_pem().decode()
|
||||||
|
@ -188,15 +192,17 @@ class Transaction(BaseTransaction):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def claim_update(
|
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):
|
funding_accounts: List[Account], change_account: Account, signing_channel: Output = None):
|
||||||
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
|
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
|
||||||
updated_claim = Output.pay_update_claim_pubkey_hash(
|
updated_claim = Output.pay_update_claim_pubkey_hash(
|
||||||
amount, previous_claim.claim_name, previous_claim.claim_id,
|
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:
|
if signing_channel is not None:
|
||||||
updated_claim.sign(signing_channel, b'placeholder txid:nout')
|
updated_claim.sign(signing_channel, b'placeholder txid:nout')
|
||||||
|
else:
|
||||||
|
updated_claim.clear_signature()
|
||||||
return cls.create(
|
return cls.create(
|
||||||
[Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account, sign=False
|
[Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account, sign=False
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import os.path
|
||||||
import hashlib
|
import hashlib
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import logging
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
import ecdsa
|
import ecdsa
|
||||||
|
|
||||||
|
@ -12,6 +15,9 @@ from lbrynet.testcase import CommandTestCase
|
||||||
from torba.client.hash import sha256, Base58
|
from torba.client.hash import sha256, Base58
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChannelCommands(CommandTestCase):
|
class ChannelCommands(CommandTestCase):
|
||||||
|
|
||||||
async def test_create_channel_names(self):
|
async def test_create_channel_names(self):
|
||||||
|
@ -69,74 +75,55 @@ class ChannelCommands(CommandTestCase):
|
||||||
|
|
||||||
async def test_setting_channel_fields(self):
|
async def test_setting_channel_fields(self):
|
||||||
values = {
|
values = {
|
||||||
'tags': ["cool", "awesome"],
|
|
||||||
'title': "Cool Channel",
|
'title': "Cool Channel",
|
||||||
'description': "Best channel on LBRY.",
|
'description': "Best channel on LBRY.",
|
||||||
'thumbnail_url': "https://co.ol/thumbnail.png",
|
'thumbnail_url': "https://co.ol/thumbnail.png",
|
||||||
|
'tags': ["cool", "awesome"],
|
||||||
'languages': ["en-US"],
|
'languages': ["en-US"],
|
||||||
'locations': ['US::Manchester'],
|
'locations': ['US::Manchester'],
|
||||||
'email': "human@email.com",
|
'email': "human@email.com",
|
||||||
'website_url': "https://co.ol",
|
'website_url': "https://co.ol",
|
||||||
'cover_url': "https://co.ol/cover.png",
|
'cover_url': "https://co.ol/cover.png",
|
||||||
|
'featured': ['cafe']
|
||||||
}
|
}
|
||||||
fixed_values = values.copy()
|
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['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')}
|
||||||
|
fixed_values['locations'] = [{'country': 'US', 'city': 'Manchester'}]
|
||||||
fixed_values['cover'] = {'url': fixed_values.pop('cover_url')}
|
fixed_values['cover'] = {'url': fixed_values.pop('cover_url')}
|
||||||
|
|
||||||
# create new channel with all fields set
|
# create new channel with all fields set
|
||||||
tx = await self.out(self.channel_create('@bigchannel', **values))
|
tx = await self.out(self.channel_create('@bigchannel', **values))
|
||||||
txo = tx['outputs'][0]
|
channel = tx['outputs'][0]['value']
|
||||||
self.assertEqual(
|
self.assertEqual(channel, {'public_key': channel['public_key'], **fixed_values})
|
||||||
txo['value'],
|
|
||||||
{'public_key': txo['value']['public_key'], **fixed_values}
|
|
||||||
)
|
|
||||||
|
|
||||||
# create channel with nothing set
|
# create channel with nothing set
|
||||||
tx = await self.out(self.channel_create('@lightchannel'))
|
tx = await self.out(self.channel_create('@lightchannel'))
|
||||||
txo = tx['outputs'][0]
|
channel = tx['outputs'][0]['value']
|
||||||
self.assertEqual(
|
self.assertEqual(channel, {'public_key': channel['public_key']})
|
||||||
txo['value'],
|
|
||||||
{'public_key': txo['value']['public_key']}
|
|
||||||
)
|
|
||||||
|
|
||||||
# create channel with just some tags
|
# create channel with just a featured claim
|
||||||
tx = await self.out(self.channel_create('@updatedchannel', tags='blah'))
|
tx = await self.out(self.channel_create('@featurechannel', featured='beef'))
|
||||||
txo = tx['outputs'][0]
|
txo = tx['outputs'][0]
|
||||||
claim_id = txo['claim_id']
|
claim_id, channel = txo['claim_id'], txo['value']
|
||||||
public_key = txo['value']['public_key']
|
fixed_values['public_key'] = channel['public_key']
|
||||||
self.assertEqual(
|
self.assertEqual(channel, {'public_key': fixed_values['public_key'], 'featured': ['beef']})
|
||||||
txo['value'],
|
|
||||||
{'public_key': public_key, 'tags': ['blah']}
|
|
||||||
)
|
|
||||||
|
|
||||||
# update channel setting all fields
|
# update channel setting all fields
|
||||||
tx = await self.out(self.channel_update(claim_id, **values))
|
tx = await self.out(self.channel_update(claim_id, **values))
|
||||||
txo = tx['outputs'][0]
|
channel = tx['outputs'][0]['value']
|
||||||
fixed_values['public_key'] = public_key
|
fixed_values['featured'].insert(0, 'beef') # existing featured claim
|
||||||
fixed_values['tags'].insert(0, 'blah') # existing tag
|
self.assertEqual(channel, fixed_values)
|
||||||
self.assertEqual(
|
|
||||||
txo['value'],
|
|
||||||
fixed_values
|
|
||||||
)
|
|
||||||
|
|
||||||
# clearing and settings tags
|
# clearing and settings featured content
|
||||||
tx = await self.out(self.channel_update(claim_id, tags='single', clear_tags=True))
|
tx = await self.out(self.channel_update(claim_id, featured='beefcafe', clear_featured=True))
|
||||||
txo = tx['outputs'][0]
|
channel = tx['outputs'][0]['value']
|
||||||
fixed_values['tags'] = ['single']
|
fixed_values['featured'] = ['beefcafe']
|
||||||
self.assertEqual(
|
self.assertEqual(channel, fixed_values)
|
||||||
txo['value'],
|
|
||||||
fixed_values
|
|
||||||
)
|
|
||||||
|
|
||||||
# reset signing key
|
# reset signing key
|
||||||
tx = await self.out(self.channel_update(claim_id, new_signing_key=True))
|
tx = await self.out(self.channel_update(claim_id, new_signing_key=True))
|
||||||
txo = tx['outputs'][0]
|
channel = tx['outputs'][0]['value']
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(channel['public_key'], fixed_values['public_key'])
|
||||||
txo['value']['public_key'],
|
|
||||||
fixed_values['public_key']
|
|
||||||
)
|
|
||||||
|
|
||||||
# send channel to someone else
|
# send channel to someone else
|
||||||
new_account = await self.out(self.daemon.jsonrpc_account_create('second account'))
|
new_account = await self.out(self.daemon.jsonrpc_account_create('second account'))
|
||||||
|
@ -168,6 +155,19 @@ class ChannelCommands(CommandTestCase):
|
||||||
|
|
||||||
class StreamCommands(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):
|
async def test_create_stream_names(self):
|
||||||
# claim new name
|
# claim new name
|
||||||
await self.stream_create('foo')
|
await self.stream_create('foo')
|
||||||
|
@ -207,17 +207,17 @@ class StreamCommands(CommandTestCase):
|
||||||
tx = await self.stream_update(claim_id, bid='3.0')
|
tx = await self.stream_update(claim_id, bid='3.0')
|
||||||
self.assertEqual(tx['outputs'][0]['amount'], '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
|
# not enough funds
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
InsufficientFundsError, "Not enough funds to cover this transaction."):
|
InsufficientFundsError, "Not enough funds to cover this transaction."):
|
||||||
await self.stream_create('foo2', '9.0')
|
await self.stream_create('foo2', '9.0')
|
||||||
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
|
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
|
# 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')
|
await self.assertBalance(self.account, '0.0')
|
||||||
self.assertEqual(len(tx['outputs']), 1) # no change
|
self.assertEqual(len(tx['outputs']), 1) # no change
|
||||||
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2)
|
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2)
|
||||||
|
@ -264,12 +264,12 @@ class StreamCommands(CommandTestCase):
|
||||||
|
|
||||||
async def test_setting_stream_fields(self):
|
async def test_setting_stream_fields(self):
|
||||||
values = {
|
values = {
|
||||||
'tags': ["cool", "awesome"],
|
|
||||||
'title': "Cool Content",
|
'title': "Cool Content",
|
||||||
'description': "Best content on LBRY.",
|
'description': "Best content on LBRY.",
|
||||||
'thumbnail_url': "https://co.ol/thumbnail.png",
|
'thumbnail_url': "https://co.ol/thumbnail.png",
|
||||||
|
'tags': ["cool", "awesome"],
|
||||||
'languages': ["en"],
|
'languages': ["en"],
|
||||||
'locations': ['{"country": "UA"}'],
|
'locations': ['{"country": "US"}'],
|
||||||
|
|
||||||
'author': "Jules Verne",
|
'author': "Jules Verne",
|
||||||
'license': 'Public Domain',
|
'license': 'Public Domain',
|
||||||
|
@ -279,16 +279,13 @@ class StreamCommands(CommandTestCase):
|
||||||
'fee_currency': 'usd',
|
'fee_currency': 'usd',
|
||||||
'fee_amount': '2.99',
|
'fee_amount': '2.99',
|
||||||
'fee_address': 'mmCsWAiXMUVecFQ3fVzUwvpT9XFMXno2Ca',
|
'fee_address': 'mmCsWAiXMUVecFQ3fVzUwvpT9XFMXno2Ca',
|
||||||
|
|
||||||
'video_width': 800,
|
|
||||||
'video_height': 600
|
|
||||||
}
|
}
|
||||||
fixed_values = values.copy()
|
fixed_values = values.copy()
|
||||||
fixed_values['languages'] = ['en']
|
fixed_values['locations'] = [{'country': 'US'}]
|
||||||
fixed_values['locations'] = [{'country': 'UA'}]
|
|
||||||
fixed_values['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')}
|
fixed_values['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')}
|
||||||
fixed_values['release_time'] = str(values['release_time'])
|
fixed_values['release_time'] = str(values['release_time'])
|
||||||
fixed_values['source'] = {
|
fixed_values['source'] = {
|
||||||
|
'hash': 'c0ddd62c7717180e7ffb8a15bb9674d3ec92592e0b7ac7d1d5289836b4553be2',
|
||||||
'media_type': 'application/octet-stream',
|
'media_type': 'application/octet-stream',
|
||||||
'size': '3'
|
'size': '3'
|
||||||
}
|
}
|
||||||
|
@ -297,57 +294,68 @@ class StreamCommands(CommandTestCase):
|
||||||
'amount': float(fixed_values.pop('fee_amount')),
|
'amount': float(fixed_values.pop('fee_amount')),
|
||||||
'currency': fixed_values.pop('fee_currency').upper()
|
'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))
|
tx = await self.out(self.stream_create('big', **values))
|
||||||
txo = tx['outputs'][0]
|
stream = tx['outputs'][0]['value']
|
||||||
stream = txo['value']
|
fixed_values['source']['name'] = stream['source']['name']
|
||||||
fixed_values['source']['sd_hash'] = stream['source']['sd_hash']
|
fixed_values['source']['sd_hash'] = stream['source']['sd_hash']
|
||||||
self.assertEqual(stream, fixed_values)
|
self.assertEqual(stream, fixed_values)
|
||||||
|
|
||||||
# create channel with nothing set
|
# create stream with nothing set
|
||||||
tx = await self.out(self.stream_create('light'))
|
tx = await self.out(self.stream_create('light'))
|
||||||
txo = tx['outputs'][0]
|
stream = tx['outputs'][0]['value']
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
txo['value'], {
|
stream, {
|
||||||
'source': {
|
'source': {
|
||||||
'size': '3',
|
'size': '3',
|
||||||
'media_type': 'application/octet-stream',
|
'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
|
# create stream with just some tags, langs and locations
|
||||||
tx = await self.out(self.stream_create('updated', tags='blah'))
|
tx = await self.out(self.stream_create('updated', tags='blah', languages='uk', locations='UA::Kyiv'))
|
||||||
txo = tx['outputs'][0]
|
txo = tx['outputs'][0]
|
||||||
claim_id = txo['claim_id']
|
claim_id, stream = txo['claim_id'], txo['value']
|
||||||
fixed_values['source']['sd_hash'] = txo['value']['source']['sd_hash']
|
fixed_values['source']['name'] = stream['source']['name']
|
||||||
|
fixed_values['source']['sd_hash'] = stream['source']['sd_hash']
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
txo['value'], {
|
stream, {
|
||||||
'source': {
|
'source': {
|
||||||
'size': '3',
|
'size': '3',
|
||||||
'media_type': 'application/octet-stream',
|
'media_type': 'application/octet-stream',
|
||||||
|
'name': fixed_values['source']['name'],
|
||||||
|
'hash': 'c0ddd62c7717180e7ffb8a15bb9674d3ec92592e0b7ac7d1d5289836b4553be2',
|
||||||
'sd_hash': fixed_values['source']['sd_hash'],
|
'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))
|
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
|
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
|
# 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]
|
txo = tx['outputs'][0]
|
||||||
fixed_values['tags'] = ['single']
|
fixed_values['tags'] = ['single']
|
||||||
|
fixed_values['languages'] = ['pt']
|
||||||
|
fixed_values['locations'] = [{'country': 'BR'}]
|
||||||
self.assertEqual(txo['value'], fixed_values)
|
self.assertEqual(txo['value'], fixed_values)
|
||||||
|
|
||||||
# send claim to someone else
|
# 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()), 2)
|
||||||
self.assertEqual(len(await self.daemon.jsonrpc_claim_list(account_id=account2_id)), 1)
|
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')
|
await self.assertBalance(self.account, '10.0')
|
||||||
|
|
||||||
tx = await self.stream_create(bid='2.5') # creates new claim
|
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]['balance_delta'], '1.5')
|
||||||
self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id)
|
self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id)
|
||||||
self.assertEqual(txs[0]['value'], '0.0')
|
self.assertEqual(txs[0]['value'], '0.0')
|
||||||
self.assertEqual(txs[0]['fee'], '-0.000184')
|
self.assertEqual(txs[0]['fee'], '-0.0002075')
|
||||||
await self.assertBalance(self.account, '8.979709')
|
await self.assertBalance(self.account, '8.9796855')
|
||||||
|
|
||||||
await self.stream_abandon(claim_id)
|
await self.stream_abandon(claim_id)
|
||||||
txs = await self.out(self.daemon.jsonrpc_transaction_list())
|
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]['abandon_info'][0]['claim_id'], claim_id)
|
||||||
self.assertEqual(txs[0]['value'], '0.0')
|
self.assertEqual(txs[0]['value'], '0.0')
|
||||||
self.assertEqual(txs[0]['fee'], '-0.000107')
|
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')
|
await self.assertBalance(self.account, '10.0')
|
||||||
tx = await self.stream_create(bid='0.0001')
|
tx = await self.stream_create(bid='0.0001')
|
||||||
await self.assertBalance(self.account, '9.979793')
|
await self.assertBalance(self.account, '9.979793')
|
||||||
|
|
|
@ -4,13 +4,13 @@ from lbrynet.schema import mime_types
|
||||||
|
|
||||||
class TestMimeTypes(unittest.TestCase):
|
class TestMimeTypes(unittest.TestCase):
|
||||||
def test_mp4_video(self):
|
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")[0])
|
||||||
self.assertEqual("video/mp4", mime_types.guess_media_type("test.MP4"))
|
self.assertEqual("video/mp4", mime_types.guess_media_type("test.MP4")[0])
|
||||||
|
|
||||||
def test_x_ext_(self):
|
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")[0])
|
||||||
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])
|
||||||
|
|
||||||
def test_octet_stream(self):
|
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.")[0])
|
||||||
self.assertEqual("application/octet-stream", mime_types.guess_media_type("test"))
|
self.assertEqual("application/octet-stream", mime_types.guess_media_type("test")[0])
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from lbrynet.schema.claim import Claim, Channel, Stream
|
from lbrynet.schema.claim import Claim, Stream
|
||||||
|
|
||||||
|
|
||||||
class TestClaimContainerAwareness(TestCase):
|
class TestClaimContainerAwareness(TestCase):
|
||||||
|
@ -9,27 +9,13 @@ class TestClaimContainerAwareness(TestCase):
|
||||||
def test_stream_claim(self):
|
def test_stream_claim(self):
|
||||||
stream = Stream()
|
stream = Stream()
|
||||||
claim = stream.claim
|
claim = stream.claim
|
||||||
self.assertTrue(claim.is_stream)
|
self.assertEqual(claim.claim_type, Claim.STREAM)
|
||||||
self.assertFalse(claim.is_channel)
|
|
||||||
claim = Claim.from_bytes(claim.to_bytes())
|
claim = Claim.from_bytes(claim.to_bytes())
|
||||||
self.assertTrue(claim.is_stream)
|
self.assertEqual(claim.claim_type, Claim.STREAM)
|
||||||
self.assertFalse(claim.is_channel)
|
|
||||||
self.assertIsNotNone(claim.stream)
|
self.assertIsNotNone(claim.stream)
|
||||||
with self.assertRaisesRegex(ValueError, 'Claim is not a channel.'):
|
with self.assertRaisesRegex(ValueError, 'Claim is not a channel.'):
|
||||||
print(claim.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):
|
class TestFee(TestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue