diff --git a/lbrynet/extras/daemon/Daemon.py b/lbrynet/extras/daemon/Daemon.py index 1e89f62c7..6819ae4c6 100644 --- a/lbrynet/extras/daemon/Daemon.py +++ b/lbrynet/extras/daemon/Daemon.py @@ -1852,7 +1852,7 @@ class Daemon(metaclass=JSONRPCServerType): @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_channel_update( self, claim_id, bid=None, account_id=None, claim_address=None, - new_signing_key=False, preview=False, **kwargs): + new_signing_key=False, preview=False, replace=False, **kwargs): """ Update an existing channel claim. @@ -1865,7 +1865,8 @@ class Daemon(metaclass=JSONRPCServerType): [--locations=...] [--clear_locations] [--email=] [--website_url=] [--thumbnail_url=] [--cover_url=] - [--account_id=] [--claim_address=] [--new_signing_key] [--preview] + [--account_id=] [--claim_address=] [--new_signing_key] + [--preview] [--replace] Options: --claim_id= : (str) claim_id of the channel to update @@ -1923,6 +1924,10 @@ class Daemon(metaclass=JSONRPCServerType): --account_id= : (str) id of the account to store channel --claim_address=: (str) address where the channel is sent --new_signing_key : (bool) generate a new signing key, will invalidate all previous publishes + --replace : (bool) instead of modifying specific values on + the channel, this will clear all existing values + and only save passed in values, useful for form + submissions where all values are always set --preview : (bool) do not broadcast the transaction Returns: {Transaction} @@ -1950,7 +1955,11 @@ class Daemon(metaclass=JSONRPCServerType): else: claim_address = old_txo.get_address(account.ledger) - claim = Claim.from_bytes(old_txo.claim.to_bytes()) + if replace: + claim = Claim() + claim.channel.public_key_bytes = old_txo.claim.channel.public_key_bytes + else: + claim = Claim.from_bytes(old_txo.claim.to_bytes()) claim.channel.update(**kwargs) tx = await Transaction.claim_update( old_txo, claim, amount, claim_address, [account], account @@ -2092,13 +2101,11 @@ class Daemon(metaclass=JSONRPCServerType): conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_publish(self, name, **kwargs): """ - Create or update a stream claim at a given name (use 'stream create/update' for more control). + Create or replace a stream claim at a given name (use 'stream create/update' for more control). Usage: publish ( | --name=) [--bid=] [--file_path=] - [--tags=...] [--clear_tags] - [--languages=...] [--clear_languages] - [--locations=...] [--clear_locations] + [--tags=...] [--languages=...] [--locations=...] [--fee_currency=] [--fee_amount=] [--fee_address=] [--title=] [--description=<description>] [--author=<author>] [--language=<language>] [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>] @@ -2122,9 +2129,7 @@ class Daemon(metaclass=JSONRPCServerType): who is not the publisher and is not represented by the channel. For example, a pdf file of 'The Odyssey' has an author of 'Homer' but may by published to a channel such as '@classics', or to no channel at all - --clear_tags : (bool) clear existing tags (prior to adding new ones) --tags=<tags> : (list) add content tags - --clear_languages : (bool) clear existing languages (prior to adding new ones) --languages=<languages> : (list) languages used by the channel, using RFC 5646 format, eg: for English `--languages=en` @@ -2132,7 +2137,6 @@ class Daemon(metaclass=JSONRPCServerType): for Spanish (Mexican) `--languages=es-MX` for Chinese (Simplified) `--languages=zh-Hans` for Chinese (Traditional) `--languages=zh-Hant` - --clear_locations : (bool) clear existing locations (prior to adding new ones) --locations=<locations> : (list) locations relevant to the stream, consisting of 2 letter `country` code and a `state`, `city` and a postal `code` along with a `latitude` and `longitude`. @@ -2193,7 +2197,7 @@ class Daemon(metaclass=JSONRPCServerType): return await self.jsonrpc_stream_create(name, **kwargs) elif len(claims) == 1: assert claims[0].claim.is_stream, f"Claim at name '{name}' is not a stream claim." - return await self.jsonrpc_stream_update(claims[0].claim_id, **kwargs) + return await self.jsonrpc_stream_update(claims[0].claim_id, replace=True, **kwargs) raise Exception( f"There are {len(claims)} claims for '{name}', please use 'stream update' command " f"to update a specific stream claim." @@ -2339,7 +2343,7 @@ class Daemon(metaclass=JSONRPCServerType): self, claim_id, bid=None, file_path=None, channel_id=None, channel_name=None, channel_account_id=None, clear_channel=False, account_id=None, claim_address=None, - preview=False, **kwargs): + preview=False, replace=False, **kwargs): """ Update an existing stream claim and if a new file is provided announce it to lbrynet. @@ -2356,7 +2360,8 @@ class Daemon(metaclass=JSONRPCServerType): [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>] [--channel_id=<channel_id>] [--channel_name=<channel_name>] [--clear_channel] [--channel_account_id=<channel_account_id>...] - [--account_id=<account_id>] [--claim_address=<claim_address>] [--preview] + [--account_id=<account_id>] [--claim_address=<claim_address>] + [--preview] [--replace] Options: --claim_id=<claim_id> : (str) id of the stream claim to update @@ -2433,6 +2438,10 @@ class Daemon(metaclass=JSONRPCServerType): --account_id=<account_id> : (str) account to use for funding the transaction --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified it will be determined automatically from the account + --replace : (bool) instead of modifying specific values on + the stream, this will clear all existing values + and only save passed in values, useful for form + submissions where all values are always set --preview : (bool) do not broadcast the transaction Returns: {Transaction} @@ -2442,7 +2451,7 @@ class Daemon(metaclass=JSONRPCServerType): existing_claims = await account.get_claims(claim_id=claim_id) if len(existing_claims) != 1: raise Exception( - f"Can't find the claim '{claim_id}' in account '{account_id}'." + f"Can't find the claim '{claim_id}' in account '{account.id}'." ) old_txo = existing_claims[0] if not old_txo.claim.is_stream: @@ -2469,8 +2478,20 @@ class Daemon(metaclass=JSONRPCServerType): if 'fee_address' in kwargs: self.valid_address_or_error(kwargs['fee_address']) - claim = Claim.from_bytes(old_txo.claim.to_bytes()) - claim.stream.update(file_path=file_path, **kwargs) + if replace: + claim = Claim() + claim.stream.message.source.CopyFrom( + old_txo.claim.stream.message.source + ) + stream_type = old_txo.claim.stream.stream_type + if stream_type: + old_stream_type = getattr(old_txo.claim.stream.message, stream_type) + new_stream_type = getattr(claim.stream.message, stream_type) + new_stream_type.CopyFrom(old_stream_type) + claim.stream.update(file_path=file_path, **kwargs) + else: + claim = Claim.from_bytes(old_txo.claim.to_bytes()) + claim.stream.update(file_path=file_path, **kwargs) tx = await Transaction.claim_update( old_txo, claim, amount, claim_address, [account], account, channel ) diff --git a/lbrynet/schema/claim.py b/lbrynet/schema/claim.py index 73b01b784..c29699d67 100644 --- a/lbrynet/schema/claim.py +++ b/lbrynet/schema/claim.py @@ -233,10 +233,11 @@ class Stream(BaseClaim): if stream_type in ('image', 'video', 'audio'): media = getattr(self, stream_type) media_args = {'file_metadata': None} - try: - media_args['file_metadata'] = binary_file_metadata(binary_file_parser(file_path)) - except: - log.exception('Could not read file metadata.') + if file_path is not None: + try: + media_args['file_metadata'] = binary_file_metadata(binary_file_parser(file_path)) + except: + log.exception('Could not read file metadata.') if isinstance(media, Playable): media_args['duration'] = duration if isinstance(media, Dimmensional): @@ -290,6 +291,10 @@ class Stream(BaseClaim): def source(self) -> Source: return Source(self.message.source) + @property + def stream_type(self) -> str: + return self.message.WhichOneof('type') + @property def image(self) -> Image: return Image(self.message.image) diff --git a/tests/integration/test_claim_commands.py b/tests/integration/test_claim_commands.py index d0ff9f3c3..0687fb444 100644 --- a/tests/integration/test_claim_commands.py +++ b/tests/integration/test_claim_commands.py @@ -125,6 +125,13 @@ class ChannelCommands(CommandTestCase): channel = tx['outputs'][0]['value'] self.assertNotEqual(channel['public_key'], fixed_values['public_key']) + # replace mode (clears everything except public_key) + tx = await self.out(self.channel_update(claim_id, replace=True, title='foo', email='new@email.com')) + self.assertEqual( + tx['outputs'][0]['value'], + {'public_key': channel['public_key'], 'title': 'foo', 'email': 'new@email.com'} + ) + # send channel to someone else new_account = await self.out(self.daemon.jsonrpc_account_create('second account')) account2_id, account2 = new_account['id'], self.daemon.get_account_or_error(new_account['id']) @@ -470,6 +477,42 @@ class StreamCommands(CommandTestCase): } ) + async def test_replace_mode_preserves_source_and_type(self): + expected = { + 'tags': ['blah'], + 'languages': ['uk'], + 'locations': [{'country': 'UA', 'city': 'Kyiv'}], + 'source': { + 'size': '2299653', + 'name': 'ForBiggerEscapes.mp4', + 'media_type': 'video/mp4', + 'hash': 'f846d9c7f5ed28f0ed47e9d9b4198a03075e6df967ac54078af85ea1bf0ddd87', + }, + 'stream_type': 'video', + 'video': { + 'width': 1280, + 'height': 720, + 'duration': 15 + } + } + tx = await self.out(self.daemon.jsonrpc_stream_create( + 'chrome', '1.0', file_path=self.video_file_name, + tags='blah', languages='uk', locations='UA::Kyiv' + )) + await self.on_transaction_dict(tx) + txo = tx['outputs'][0] + expected['source']['sd_hash'] = txo['value']['source']['sd_hash'] + self.assertEqual(txo['value'], expected) + tx = await self.out(self.daemon.jsonrpc_stream_update( + txo['claim_id'], title='new title', replace=True + )) + txo = tx['outputs'][0] + expected['title'] = 'new title' + del expected['tags'] + del expected['languages'] + del expected['locations'] + self.assertEqual(txo['value'], expected) + async def test_create_update_and_abandon_stream(self): await self.assertBalance(self.account, '10.0')