• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lbryio / lbry-sdk / 3708794183

pending completion
3708794183

Pull #3657

github

GitHub
Merge 636b7ed47 into 625865165
Pull Request #3657: wip: add initial support for streaming torrent files

2754 of 6491 branches covered (42.43%)

Branch coverage included in aggregate %.

64 of 245 new or added lines in 12 files covered. (26.12%)

20 existing lines in 5 files now uncovered.

12055 of 19808 relevant lines covered (60.86%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.04
/lbry/extras/daemon/json_response_encoder.py
1
import logging
1✔
2
from decimal import Decimal
1✔
3
from binascii import hexlify, unhexlify
1✔
4
from datetime import datetime
1✔
5
from json import JSONEncoder
1✔
6

7
from google.protobuf.message import DecodeError
1✔
8

9
from lbry.schema.claim import Claim
1✔
10
from lbry.schema.support import Support
1✔
11
from lbry.torrent.torrent_manager import TorrentSource
1✔
12
from lbry.wallet import Wallet, Ledger, Account, Transaction, Output
1✔
13
from lbry.wallet.bip32 import PublicKey
1✔
14
from lbry.wallet.dewies import dewies_to_lbc
1✔
15
from lbry.stream.managed_stream import ManagedStream
1✔
16

17

18
log = logging.getLogger(__name__)
1✔
19

20

21
def encode_txo_doc():
1✔
22
    return {
×
23
        'txid': "hash of transaction in hex",
24
        'nout': "position in the transaction",
25
        'height': "block where transaction was recorded",
26
        'amount': "value of the txo as a decimal",
27
        'address': "address of who can spend the txo",
28
        'confirmations': "number of confirmed blocks",
29
        'is_change': "payment to change address, only available when it can be determined",
30
        'is_received': "true if txo was sent from external account to this account",
31
        'is_spent': "true if txo is spent",
32
        'is_mine': "payment to one of your accounts, only available when it can be determined",
33
        'type': "one of 'claim', 'support' or 'purchase'",
34
        'name': "when type is 'claim' or 'support', this is the claim name",
35
        'claim_id': "when type is 'claim', 'support' or 'purchase', this is the claim id",
36
        'claim_op': "when type is 'claim', this determines if it is 'create' or 'update'",
37
        'value': "when type is 'claim' or 'support' with payload, this is the decoded protobuf payload",
38
        'value_type': "determines the type of the 'value' field: 'channel', 'stream', etc",
39
        'protobuf': "hex encoded raw protobuf version of 'value' field",
40
        'permanent_url': "when type is 'claim' or 'support', this is the long permanent claim URL",
41
        'claim': "for purchase outputs only, metadata of purchased claim",
42
        'reposted_claim': "for repost claims only, metadata of claim being reposted",
43
        'signing_channel': "for signed claims only, metadata of signing channel",
44
        'is_channel_signature_valid': "for signed claims only, whether signature is valid",
45
        'purchase_receipt': "metadata for the purchase transaction associated with this claim"
46
    }
47

48

49
def encode_tx_doc():
1✔
50
    return {
×
51
        'txid': "hash of transaction in hex",
52
        'height': "block where transaction was recorded",
53
        'inputs': [encode_txo_doc()],
54
        'outputs': [encode_txo_doc()],
55
        'total_input': "sum of inputs as a decimal",
56
        'total_output': "sum of outputs, sans fee, as a decimal",
57
        'total_fee': "fee amount",
58
        'hex': "entire transaction encoded in hex",
59
    }
60

61

62
def encode_account_doc():
1✔
63
    return {
×
64
        'id': 'account_id',
65
        'is_default': 'this account is used by default',
66
        'ledger': 'name of crypto currency and network',
67
        'name': 'optional account name',
68
        'seed': 'human friendly words from which account can be recreated',
69
        'encrypted': 'if account is encrypted',
70
        'private_key': 'extended private key',
71
        'public_key': 'extended public key',
72
        'address_generator': 'settings for generating addresses',
73
        'modified_on': 'date of last modification to account settings'
74
    }
75

76

77
def encode_wallet_doc():
1✔
78
    return {
×
79
        'id': 'wallet_id',
80
        'name': 'optional wallet name',
81
    }
82

83

84
def encode_file_doc():
1✔
85
    return {
×
86
        'streaming_url': '(str) url to stream the file using range requests',
87
        'completed': '(bool) true if download is completed',
88
        'file_name': '(str) name of file',
89
        'download_directory': '(str) download directory',
90
        'points_paid': '(float) credit paid to download file',
91
        'stopped': '(bool) true if download is stopped',
92
        'stream_hash': '(str) stream hash of file',
93
        'stream_name': '(str) stream name',
94
        'suggested_file_name': '(str) suggested file name',
95
        'sd_hash': '(str) sd hash of file',
96
        'download_path': '(str) download path of file',
97
        'mime_type': '(str) mime type of file',
98
        'key': '(str) key attached to file',
99
        'total_bytes_lower_bound': '(int) lower bound file size in bytes',
100
        'total_bytes': '(int) file upper bound size in bytes',
101
        'written_bytes': '(int) written size in bytes',
102
        'blobs_completed': '(int) number of fully downloaded blobs',
103
        'blobs_in_stream': '(int) total blobs on stream',
104
        'blobs_remaining': '(int) total blobs remaining to download',
105
        'status': '(str) downloader status',
106
        'claim_id': '(str) None if claim is not found else the claim id',
107
        'txid': '(str) None if claim is not found else the transaction id',
108
        'nout': '(int) None if claim is not found else the transaction output index',
109
        'outpoint': '(str) None if claim is not found else the tx and output',
110
        'metadata': '(dict) None if claim is not found else the claim metadata',
111
        'channel_claim_id': '(str) None if claim is not found or not signed',
112
        'channel_name': '(str) None if claim is not found or not signed',
113
        'claim_name': '(str) None if claim is not found else the claim name',
114
        'reflector_progress': '(int) reflector upload progress, 0 to 100',
115
        'uploading_to_reflector': '(bool) set to True when currently uploading to reflector'
116
    }
117

118

119
class JSONResponseEncoder(JSONEncoder):
1✔
120

121
    def __init__(self, *args, ledger: Ledger, include_protobuf=False, **kwargs):
1✔
122
        super().__init__(*args, **kwargs)
1✔
123
        self.ledger = ledger
1✔
124
        self.include_protobuf = include_protobuf
1✔
125

126
    def default(self, obj):  # pylint: disable=method-hidden,arguments-renamed,too-many-return-statements
1✔
127
        if isinstance(obj, Account):
×
128
            return self.encode_account(obj)
×
129
        if isinstance(obj, Wallet):
×
130
            return self.encode_wallet(obj)
×
131
        if isinstance(obj, (ManagedStream, TorrentSource)):
×
132
            return self.encode_file(obj)
×
133
        if isinstance(obj, Transaction):
×
134
            return self.encode_transaction(obj)
×
135
        if isinstance(obj, Output):
×
136
            return self.encode_output(obj)
×
137
        if isinstance(obj, Claim):
×
138
            return self.encode_claim(obj)
×
139
        if isinstance(obj, Support):
×
140
            return obj.to_dict()
×
141
        if isinstance(obj, PublicKey):
×
142
            return obj.extended_key_string()
×
143
        if isinstance(obj, datetime):
×
144
            return obj.strftime("%Y%m%dT%H:%M:%S")
×
145
        if isinstance(obj, Decimal):
×
146
            return float(obj)
×
147
        if isinstance(obj, bytes):
×
148
            return obj.decode()
×
149
        return super().default(obj)
×
150

151
    def encode_transaction(self, tx):
1✔
152
        return {
×
153
            'txid': tx.id,
154
            'height': tx.height,
155
            'inputs': [self.encode_input(txo) for txo in tx.inputs],
156
            'outputs': [self.encode_output(txo) for txo in tx.outputs],
157
            'total_input': dewies_to_lbc(tx.input_sum),
158
            'total_output': dewies_to_lbc(tx.input_sum - tx.fee),
159
            'total_fee': dewies_to_lbc(tx.fee),
160
            'hex': hexlify(tx.raw).decode(),
161
        }
162

163
    def encode_output(self, txo, check_signature=True):
1✔
164
        if not txo:
×
165
            return
×
166
        tx_height = txo.tx_ref.height
×
167
        best_height = self.ledger.headers.height
×
168
        output = {
×
169
            'txid': txo.tx_ref.id,
170
            'nout': txo.position,
171
            'height': tx_height,
172
            'amount': dewies_to_lbc(txo.amount),
173
            'address': txo.get_address(self.ledger) if txo.has_address else None,
174
            'confirmations': (best_height+1) - tx_height if tx_height > 0 else tx_height,
175
            'timestamp': self.ledger.headers.estimated_timestamp(tx_height)
176
        }
177
        if txo.is_spent is not None:
×
178
            output['is_spent'] = txo.is_spent
×
179
        if txo.is_my_output is not None:
×
180
            output['is_my_output'] = txo.is_my_output
×
181
        if txo.is_my_input is not None:
×
182
            output['is_my_input'] = txo.is_my_input
×
183
        if txo.sent_supports is not None:
×
184
            output['sent_supports'] = dewies_to_lbc(txo.sent_supports)
×
185
        if txo.sent_tips is not None:
×
186
            output['sent_tips'] = dewies_to_lbc(txo.sent_tips)
×
187
        if txo.received_tips is not None:
×
188
            output['received_tips'] = dewies_to_lbc(txo.received_tips)
×
189
        if txo.is_internal_transfer is not None:
×
190
            output['is_internal_transfer'] = txo.is_internal_transfer
×
191

192
        if txo.script.is_claim_name:
×
193
            output['type'] = 'claim'
×
194
            output['claim_op'] = 'create'
×
195
        elif txo.script.is_update_claim:
×
196
            output['type'] = 'claim'
×
197
            output['claim_op'] = 'update'
×
198
        elif txo.script.is_support_claim:
×
199
            output['type'] = 'support'
×
200
        elif txo.script.is_return_data:
×
201
            output['type'] = 'data'
×
202
        elif txo.purchase is not None:
×
203
            output['type'] = 'purchase'
×
204
            output['claim_id'] = txo.purchased_claim_id
×
205
            if txo.purchased_claim is not None:
×
206
                output['claim'] = self.encode_output(txo.purchased_claim)
×
207
        else:
208
            output['type'] = 'payment'
×
209

210
        if txo.script.is_claim_involved:
×
211
            output.update({
×
212
                'name': txo.claim_name,
213
                'normalized_name': txo.normalized_name,
214
                'claim_id': txo.claim_id,
215
                'permanent_url': txo.permanent_url,
216
                'meta': self.encode_claim_meta(txo.meta.copy())
217
            })
218
            if 'short_url' in output['meta']:
×
219
                output['short_url'] = output['meta'].pop('short_url')
×
220
            if 'canonical_url' in output['meta']:
×
221
                output['canonical_url'] = output['meta'].pop('canonical_url')
×
222
            if txo.claims is not None:
×
223
                output['claims'] = [self.encode_output(o) for o in txo.claims]
×
224
            if txo.reposted_claim is not None:
×
225
                output['reposted_claim'] = self.encode_output(txo.reposted_claim)
×
226
        if txo.script.is_claim_name or txo.script.is_update_claim or txo.script.is_support_claim_data:
×
227
            try:
×
228
                output['value'] = txo.signable
×
229
                if self.include_protobuf:
×
230
                    output['protobuf'] = hexlify(txo.signable.to_bytes())
×
231
                if txo.purchase_receipt is not None:
×
232
                    output['purchase_receipt'] = self.encode_output(txo.purchase_receipt)
×
233
                if txo.script.is_claim_name or txo.script.is_update_claim:
×
234
                    output['value_type'] = txo.claim.claim_type
×
235
                    if txo.claim.is_channel:
×
236
                        output['has_signing_key'] = txo.has_private_key
×
237
                if check_signature and txo.signable.is_signed:
×
238
                    if txo.channel is not None:
×
239
                        output['signing_channel'] = self.encode_output(txo.channel)
×
240
                        output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.ledger)
×
241
                    else:
242
                        output['signing_channel'] = {'channel_id': txo.signable.signing_channel_id}
×
243
                        output['is_channel_signature_valid'] = False
×
244
            except DecodeError:
×
245
                pass
×
246
        return output
×
247

248
    def encode_claim_meta(self, meta):
1✔
249
        for key, value in meta.items():
×
250
            if key.endswith('_amount'):
×
251
                if isinstance(value, int):
×
252
                    meta[key] = dewies_to_lbc(value)
×
253
        if 0 < meta.get('creation_height', 0) <= self.ledger.headers.height:
×
254
            meta['creation_timestamp'] = self.ledger.headers.estimated_timestamp(meta['creation_height'])
×
255
        return meta
×
256

257
    def encode_input(self, txi):
1✔
258
        return self.encode_output(txi.txo_ref.txo, False) if txi.txo_ref.txo is not None else {
×
259
            'txid': txi.txo_ref.tx_ref.id,
260
            'nout': txi.txo_ref.position
261
        }
262

263
    def encode_account(self, account):
1✔
264
        result = account.to_dict()
×
265
        result['id'] = account.id
×
266
        result.pop('certificates', None)
×
267
        result['is_default'] = self.ledger.accounts[0] == account
×
268
        return result
×
269

270
    @staticmethod
1✔
271
    def encode_wallet(wallet):
272
        return {
×
273
            'id': wallet.id,
274
            'name': wallet.name
275
        }
276

277
    def encode_file(self, managed_stream):
1✔
278
        output_exists = managed_stream.output_file_exists
×
279
        tx_height = managed_stream.stream_claim_info.height
×
280
        best_height = self.ledger.headers.height
×
281
        is_stream = hasattr(managed_stream, 'stream_hash')
×
282
        if is_stream:
×
283
            total_bytes_lower_bound = managed_stream.descriptor.lower_bound_decrypted_length()
×
284
            total_bytes = managed_stream.descriptor.upper_bound_decrypted_length()
×
285
        else:
286
            total_bytes_lower_bound = total_bytes = managed_stream.torrent_length
×
287
        result = {
×
288
            'streaming_url': managed_stream.stream_url,
289
            'completed': managed_stream.completed,
290
            'file_name': None,
291
            'download_directory': None,
292
            'download_path': None,
293
            'points_paid': 0.0,
294
            'stopped': not managed_stream.running,
295
            'stream_hash': None,
296
            'stream_name': managed_stream.stream_name,
297
            'suggested_file_name': managed_stream.suggested_file_name,
298
            'sd_hash': None,
299
            'mime_type': managed_stream.mime_type,
300
            'key': None,
301
            'total_bytes_lower_bound': total_bytes_lower_bound,
302
            'total_bytes': total_bytes,
303
            'written_bytes': managed_stream.written_bytes,
304
            'blobs_completed': None,
305
            'blobs_in_stream': None,
306
            'blobs_remaining': None,
307
            'status': managed_stream.status,
308
            'claim_id': managed_stream.claim_id,
309
            'txid': managed_stream.txid,
310
            'nout': managed_stream.nout,
311
            'outpoint': managed_stream.outpoint,
312
            'metadata': managed_stream.metadata,
313
            'protobuf': managed_stream.metadata_protobuf,
314
            'channel_claim_id': managed_stream.channel_claim_id,
315
            'channel_name': managed_stream.channel_name,
316
            'claim_name': managed_stream.claim_name,
317
            'content_fee': managed_stream.content_fee,
318
            'purchase_receipt': self.encode_output(managed_stream.purchase_receipt),
319
            'added_on': managed_stream.added_on,
320
            'height': tx_height,
321
            'confirmations': (best_height + 1) - tx_height if tx_height > 0 else tx_height,
322
            'timestamp': self.ledger.headers.estimated_timestamp(tx_height),
323
            'is_fully_reflected': False,
324
            'reflector_progress': False,
325
            'uploading_to_reflector': False
326
        }
327
        if is_stream:
×
328
            result.update({
×
329
                'stream_hash': managed_stream.stream_hash,
330
                'sd_hash': managed_stream.descriptor.sd_hash,
331
                'key': managed_stream.descriptor.key,
332
                'blobs_completed': managed_stream.blobs_completed,
333
                'blobs_in_stream': managed_stream.blobs_in_stream,
334
                'blobs_remaining': managed_stream.blobs_remaining,
335
                'is_fully_reflected': managed_stream.is_fully_reflected,
336
                'reflector_progress': managed_stream.reflector_progress,
337
                'uploading_to_reflector': managed_stream.uploading_to_reflector
338
            })
UNCOV
339
        if output_exists:
×
340
            result.update({
×
341
                'file_name': managed_stream.file_name,
342
                'download_directory': managed_stream.download_directory,
343
                'download_path': managed_stream.full_path,
344
            })
345
        return result
×
346

347
    def encode_claim(self, claim):
1✔
348
        encoded = getattr(claim, claim.claim_type).to_dict()
×
349
        if 'public_key' in encoded:
×
350
            encoded['public_key_id'] = self.ledger.public_key_to_address(
×
351
                unhexlify(encoded['public_key'])
352
            )
353
        return encoded
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc