Compare commits
243 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c7dc03d06 | ||
|
|
f511049eaa | ||
|
|
76118ba196 | ||
|
|
83b10ab2f3 | ||
|
|
604c52e608 | ||
|
|
8949599232 | ||
|
|
c9c235061c | ||
|
|
328889689f | ||
|
|
e865c25a4e | ||
|
|
6d581c22f9 | ||
|
|
87aadeeb58 | ||
|
|
d331131cfe | ||
|
|
0dce1b009a | ||
|
|
bd77cd2058 | ||
|
|
8e47e8d5db | ||
|
|
946dd155d8 | ||
|
|
3565153597 | ||
|
|
f6c6ec5603 | ||
|
|
1cd0bab60b | ||
|
|
1c246eea6d | ||
|
|
3a365243d9 | ||
|
|
7f8882c9e9 | ||
|
|
bc401ac80f | ||
|
|
0a9837adca | ||
|
|
f4bf30c0e9 | ||
|
|
acfba74821 | ||
|
|
f46845c777 | ||
|
|
74f1536553 | ||
|
|
af4c2fe4b9 | ||
|
|
b7c656536d | ||
|
|
faf875c0f5 | ||
|
|
b3ebe3879d | ||
|
|
da79895e55 | ||
|
|
aaa7421fdf | ||
|
|
b9f213f047 | ||
|
|
fee070b299 | ||
|
|
275e28b635 | ||
|
|
808e599be6 | ||
|
|
5cb6f7f123 | ||
|
|
a2f1c658f0 | ||
|
|
05de644d77 | ||
|
|
b908855566 | ||
|
|
8d93bfcb95 | ||
|
|
bf68859f38 | ||
|
|
78fbe97b66 | ||
|
|
166a256c1c | ||
|
|
b7f2967a4f | ||
|
|
0a8ff3abdc | ||
|
|
9b38a5428d | ||
|
|
9311bf1993 | ||
|
|
ee839da7c9 | ||
|
|
2ae77b3850 | ||
|
|
afd7cea635 | ||
|
|
9018ff9ee4 | ||
|
|
b676912d64 | ||
|
|
d75e1db304 | ||
|
|
0d7dd26ede | ||
|
|
24b9f45e4a | ||
|
|
821cc57024 | ||
|
|
f050f5d650 | ||
|
|
c4eb78e112 | ||
|
|
40b0138a92 | ||
|
|
1d80dce913 | ||
|
|
8b18f4476a | ||
|
|
35a27fee3e | ||
|
|
eca766ff1a | ||
|
|
139dc7fce2 | ||
|
|
2c8a883ee3 | ||
|
|
865e4b5fab | ||
|
|
02d48ceccc | ||
|
|
bc3f16e86b | ||
|
|
1e6ce58f8b | ||
|
|
4db9486853 | ||
|
|
775b79642e | ||
|
|
423a128882 | ||
|
|
aaf9860bdc | ||
|
|
83ad4dcf1f | ||
|
|
765251b872 | ||
|
|
7ea88fead2 | ||
|
|
ea67e3e54e | ||
|
|
a5c7ba52c8 | ||
|
|
7cf780ee87 | ||
|
|
c37201fa03 | ||
|
|
6fcc1393de | ||
|
|
a474899268 | ||
|
|
3f72eb0e00 | ||
|
|
661d1e4b81 | ||
|
|
4652a56bc6 | ||
|
|
966320cab5 | ||
|
|
35218a66da | ||
|
|
3432609588 | ||
|
|
3ad6b7e845 | ||
|
|
48669e5f53 | ||
|
|
7b0708ec46 | ||
|
|
f46df3ae42 | ||
|
|
96c028bd5d | ||
|
|
402dc15d7a | ||
|
|
6088ab6932 | ||
|
|
13812bdad3 | ||
|
|
d98d34d8b3 | ||
|
|
24fa104e84 | ||
|
|
b4dad8c641 | ||
|
|
3550cd6d91 | ||
|
|
2815b48e0e | ||
|
|
650e6ccb65 | ||
|
|
4a00a19a43 | ||
|
|
b067eda7b6 | ||
|
|
1b6bc86e76 | ||
|
|
da2b513bcc | ||
|
|
6adae578ef | ||
|
|
128a834841 | ||
|
|
086a14115f | ||
|
|
6a392f3e1a | ||
|
|
93127a703c | ||
|
|
e4ddbaf8ae | ||
|
|
ec75058605 | ||
|
|
2b62e5dc5e | ||
|
|
8d7874096e | ||
|
|
99fcab83c8 | ||
|
|
3027bc0579 | ||
|
|
b1b70a4e76 | ||
|
|
de41341d84 | ||
|
|
a03d43b081 | ||
|
|
f60aaade7f | ||
|
|
d3c34086ff | ||
|
|
6b58c9bcf5 | ||
|
|
c2cba1651e | ||
|
|
ada3eb437d | ||
|
|
c1517d5be8 | ||
|
|
351034d1e6 | ||
|
|
c1db5a0c47 | ||
|
|
088dce712a | ||
|
|
425e880b09 | ||
|
|
62ec78abee | ||
|
|
c84a32682c | ||
|
|
74277b2afe | ||
|
|
cd20b74b2a | ||
|
|
06f54fd985 | ||
|
|
98b0470703 | ||
|
|
bb4113b53c | ||
|
|
07f4382ed4 | ||
|
|
d40720616b | ||
|
|
eebe7c79bd | ||
|
|
6c9e327e36 | ||
|
|
e9161c0ddd | ||
|
|
c8b75dcf0e | ||
|
|
30cb7d7043 | ||
|
|
19d5b74beb | ||
|
|
d5c3e45edc | ||
|
|
1d479fc15c | ||
|
|
20a20ddd08 | ||
|
|
00c239f974 | ||
|
|
67b766b32c | ||
|
|
249aa0d147 | ||
|
|
c708a588d8 | ||
|
|
cb15df525f | ||
|
|
fcddc1516b | ||
|
|
a7732efd07 | ||
|
|
0a2f4e8418 | ||
|
|
0c0ba0dfe6 | ||
|
|
02827b174e | ||
|
|
81dee8a218 | ||
|
|
5eb8bdbd0e | ||
|
|
a37602e666 | ||
|
|
306b69198e | ||
|
|
175e457052 | ||
|
|
5633a48618 | ||
|
|
d7e608e8a1 | ||
|
|
213427fab3 | ||
|
|
3427c6fb69 | ||
|
|
603c4470b7 | ||
|
|
37c8b7ae45 | ||
|
|
d362152c77 | ||
|
|
8f5c3f312a | ||
|
|
15a1d5c210 | ||
|
|
499cf26fa8 | ||
|
|
90596be880 | ||
|
|
50d7b097e6 | ||
|
|
b8d5ec5465 | ||
|
|
3200c5654f | ||
|
|
4905b1e4d8 | ||
|
|
16df63c14e | ||
|
|
e950dff9d2 | ||
|
|
39d99ad4af | ||
|
|
3675c91240 | ||
|
|
46258f625a | ||
|
|
2cc161b589 | ||
|
|
115277e5e1 | ||
|
|
ebf0e7c181 | ||
|
|
b418898eef | ||
|
|
3106b3e545 | ||
|
|
50816a661d | ||
|
|
6755bc8bb2 | ||
|
|
d62e7730ab | ||
|
|
26be989b9b | ||
|
|
73ad0a1f44 | ||
|
|
66b185ebf7 | ||
|
|
8bd82713e2 | ||
|
|
71650c39f7 | ||
|
|
488445c73b | ||
|
|
075e811efe | ||
|
|
9f9b83f185 | ||
|
|
58d9bf7fdb | ||
|
|
b3e6275de7 | ||
|
|
748778f545 | ||
|
|
b2a68d0a74 | ||
|
|
e29b3b8377 | ||
|
|
0859ed5fb1 | ||
|
|
a80d5ba080 | ||
|
|
ac2924824e | ||
|
|
b7e6043a71 | ||
|
|
820ba35013 | ||
|
|
ecd2d130bf | ||
|
|
1d410b6e68 | ||
|
|
f77a2c889b | ||
|
|
47d5ab288f | ||
|
|
5f53fd24dd | ||
|
|
11a9d0e2d7 | ||
|
|
6f18de46f7 | ||
|
|
480c9e15b8 | ||
|
|
35aa7636f6 | ||
|
|
8fee67c2d4 | ||
|
|
74bfdd07e2 | ||
|
|
d3f1643a40 | ||
|
|
eb29f27493 | ||
|
|
8adf75ab83 | ||
|
|
2e05803d75 | ||
|
|
f16c0ee73a | ||
|
|
a338f2b782 | ||
|
|
864ccddfd7 | ||
|
|
339df69e36 | ||
|
|
76a5b0cd18 | ||
|
|
be0ab2431b | ||
|
|
2edb60c592 | ||
|
|
2c6c3a1ca3 | ||
|
|
4be540793d | ||
|
|
08b86fe596 | ||
|
|
157f3b9952 | ||
|
|
8f3ca2662a | ||
|
|
c4b015861c | ||
|
|
3aa413d59e | ||
|
|
03ba285a16 | ||
|
|
5fe0ee5aa8 |
27
.github/workflows/run_test.yml
vendored
Normal file
27
.github/workflows/run_test.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Run All UnitTest
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
python-version: [3.7, 3.8, 3.9]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt -r requirements_test.txt
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
export PYTHONPATH=./
|
||||||
|
pytest --verbose --color=yes
|
||||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
#logs
|
||||||
|
log.txt
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# develop
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Pypi dist
|
||||||
|
dist/
|
||||||
|
README.rst
|
||||||
|
temporary/
|
||||||
|
# Pypi wheel
|
||||||
|
build/
|
||||||
|
exclude/
|
||||||
13
Pipfile
Normal file
13
Pipfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
httpx = {extras = ["http2"]}
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
pytest-mock = "*"
|
||||||
|
pytest-httpx = "*"
|
||||||
|
wheel = "*"
|
||||||
|
twine = "*"
|
||||||
410
Pipfile.lock
generated
Normal file
410
Pipfile.lock
generated
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "74b83f2e50bc16f8d90c06ddc775d24ee427f8481a2501f62170bf5b76a2f1bd"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"anyio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0",
|
||||||
|
"sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.6.2'",
|
||||||
|
"version": "==3.3.0"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
|
||||||
|
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
|
||||||
|
],
|
||||||
|
"version": "==2021.5.30"
|
||||||
|
},
|
||||||
|
"h11": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||||
|
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.12.0"
|
||||||
|
},
|
||||||
|
"h2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5",
|
||||||
|
"sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"
|
||||||
|
],
|
||||||
|
"version": "==3.2.0"
|
||||||
|
},
|
||||||
|
"hpack": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89",
|
||||||
|
"sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"
|
||||||
|
],
|
||||||
|
"version": "==3.0.0"
|
||||||
|
},
|
||||||
|
"httpcore": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e",
|
||||||
|
"sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.13.6"
|
||||||
|
},
|
||||||
|
"httpx": {
|
||||||
|
"extras": [
|
||||||
|
"http2"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c",
|
||||||
|
"sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.18.2"
|
||||||
|
},
|
||||||
|
"hyperframe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40",
|
||||||
|
"sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"
|
||||||
|
],
|
||||||
|
"version": "==5.2.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||||
|
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||||
|
],
|
||||||
|
"version": "==3.2"
|
||||||
|
},
|
||||||
|
"rfc3986": {
|
||||||
|
"extras": [
|
||||||
|
"idna2008"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||||
|
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||||
|
],
|
||||||
|
"version": "==1.5.0"
|
||||||
|
},
|
||||||
|
"sniffio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||||
|
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"anyio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0",
|
||||||
|
"sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.6.2'",
|
||||||
|
"version": "==3.3.0"
|
||||||
|
},
|
||||||
|
"atomicwrites": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197",
|
||||||
|
"sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"
|
||||||
|
],
|
||||||
|
"markers": "sys_platform == 'win32'",
|
||||||
|
"version": "==1.4.0"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||||
|
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==21.2.0"
|
||||||
|
},
|
||||||
|
"bleach": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:306483a5a9795474160ad57fce3ddd1b50551e981eed8e15a582d34cef28aafa",
|
||||||
|
"sha256:ae976d7174bba988c0b632def82fdc94235756edfb14e6558a9c5be555c9fb78"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==3.3.1"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
|
||||||
|
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
|
||||||
|
],
|
||||||
|
"version": "==2021.5.30"
|
||||||
|
},
|
||||||
|
"charset-normalizer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1",
|
||||||
|
"sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3'",
|
||||||
|
"version": "==2.0.3"
|
||||||
|
},
|
||||||
|
"colorama": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
||||||
|
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
|
||||||
|
],
|
||||||
|
"markers": "platform_system == 'Windows' and sys_platform == 'win32'",
|
||||||
|
"version": "==0.4.4"
|
||||||
|
},
|
||||||
|
"docutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
|
||||||
|
"sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==0.17.1"
|
||||||
|
},
|
||||||
|
"h11": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||||
|
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.12.0"
|
||||||
|
},
|
||||||
|
"httpcore": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e",
|
||||||
|
"sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.13.6"
|
||||||
|
},
|
||||||
|
"httpx": {
|
||||||
|
"extras": [
|
||||||
|
"http2"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c",
|
||||||
|
"sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.18.2"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||||
|
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||||
|
],
|
||||||
|
"version": "==3.2"
|
||||||
|
},
|
||||||
|
"importlib-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac",
|
||||||
|
"sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==4.6.1"
|
||||||
|
},
|
||||||
|
"iniconfig": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||||
|
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||||
|
],
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
|
"keyring": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8",
|
||||||
|
"sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==23.0.1"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||||
|
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==21.0"
|
||||||
|
},
|
||||||
|
"pkginfo": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779",
|
||||||
|
"sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"
|
||||||
|
],
|
||||||
|
"version": "==1.7.1"
|
||||||
|
},
|
||||||
|
"pluggy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||||
|
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==0.13.1"
|
||||||
|
},
|
||||||
|
"py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||||
|
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.10.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
|
||||||
|
"sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==2.9.0"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||||
|
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==2.4.7"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
|
||||||
|
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==6.2.4"
|
||||||
|
},
|
||||||
|
"pytest-httpx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e135b8779060091fa1c87d8aff7904921e8bea95fce5e971a0262764d064b12",
|
||||||
|
"sha256:e262932f2d3ce380da8273c7bacbcfdc2c94e167fa94da29571caaf1f4d3ba27"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.12.0"
|
||||||
|
},
|
||||||
|
"pytest-mock": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3",
|
||||||
|
"sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.6.1"
|
||||||
|
},
|
||||||
|
"pywin32-ctypes": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
|
||||||
|
"sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
|
||||||
|
],
|
||||||
|
"markers": "sys_platform == 'win32'",
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"readme-renderer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c",
|
||||||
|
"sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"
|
||||||
|
],
|
||||||
|
"version": "==29.0"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
|
||||||
|
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||||
|
"version": "==2.26.0"
|
||||||
|
},
|
||||||
|
"requests-toolbelt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
|
||||||
|
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||||
|
],
|
||||||
|
"version": "==0.9.1"
|
||||||
|
},
|
||||||
|
"rfc3986": {
|
||||||
|
"extras": [
|
||||||
|
"idna2008"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||||
|
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||||
|
],
|
||||||
|
"version": "==1.5.0"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||||
|
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.16.0"
|
||||||
|
},
|
||||||
|
"sniffio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||||
|
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==1.2.0"
|
||||||
|
},
|
||||||
|
"toml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||||
|
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==0.10.2"
|
||||||
|
},
|
||||||
|
"tqdm": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5aa445ea0ad8b16d82b15ab342de6b195a722d75fc1ef9934a46bba6feafbc64",
|
||||||
|
"sha256:8bb94db0d4468fea27d004a0f1d1c02da3cdedc00fe491c0de986b76a04d6b0a"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==4.61.2"
|
||||||
|
},
|
||||||
|
"twine": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218",
|
||||||
|
"sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.4.2"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
|
||||||
|
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||||
|
"version": "==1.26.6"
|
||||||
|
},
|
||||||
|
"webencodings": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||||
|
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||||
|
],
|
||||||
|
"version": "==0.5.1"
|
||||||
|
},
|
||||||
|
"wheel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e",
|
||||||
|
"sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.36.2"
|
||||||
|
},
|
||||||
|
"zipp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
|
||||||
|
"sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
176
README.md
176
README.md
@@ -2,12 +2,12 @@ pytchat
|
|||||||
=======
|
=======
|
||||||
|
|
||||||
pytchat is a python library for fetching youtube live chat.
|
pytchat is a python library for fetching youtube live chat.
|
||||||
|
|
||||||
|
|
||||||
|
<br><br><br>
|
||||||
## Description
|
## Description
|
||||||
pytchat is a python library for fetching youtube live chat
|
pytchat is a python library for fetching youtube live chat
|
||||||
without using youtube api, Selenium or BeautifulSoup.
|
without using Selenium or BeautifulSoup.
|
||||||
|
|
||||||
pytchatは、YouTubeチャットを閲覧するためのpythonライブラリです。
|
|
||||||
|
|
||||||
Other features:
|
Other features:
|
||||||
+ Customizable [chat data processors](https://github.com/taizan-hokuto/pytchat/wiki/ChatProcessor) including youtube api compatible one.
|
+ Customizable [chat data processors](https://github.com/taizan-hokuto/pytchat/wiki/ChatProcessor) including youtube api compatible one.
|
||||||
@@ -16,7 +16,7 @@ Other features:
|
|||||||
instead of web scraping.
|
instead of web scraping.
|
||||||
|
|
||||||
For more detailed information, see [wiki](https://github.com/taizan-hokuto/pytchat/wiki). <br>
|
For more detailed information, see [wiki](https://github.com/taizan-hokuto/pytchat/wiki). <br>
|
||||||
より詳細な解説は[wiki](https://github.com/taizan-hokuto/pytchat/wiki/Home_jp)を参照してください。
|
[wiki (Japanese)](https://github.com/taizan-hokuto/pytchat/wiki/Home_jp)
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
```python
|
```python
|
||||||
@@ -24,147 +24,44 @@ pip install pytchat
|
|||||||
```
|
```
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### CLI
|
|
||||||
|
|
||||||
One-liner command.
|
### Fetch chat data (see [wiki](https://github.com/taizan-hokuto/pytchat/wiki/PytchatCore))
|
||||||
Save chat data to html, with embedded custom emojis.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ pytchat -v https://www.youtube.com/watch?v=ZJ6Q4U_Vg6s -o "c:/temp/"
|
|
||||||
# options:
|
|
||||||
# -v : Video ID or URL that includes ID
|
|
||||||
# -o : output directory (default path: './')
|
|
||||||
# saved filename is [video_id].html
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### on-demand mode
|
|
||||||
```python
|
```python
|
||||||
from pytchat import LiveChat
|
import pytchat
|
||||||
livechat = LiveChat(video_id = "Zvp1pJpie4I")
|
chat = pytchat.create(video_id="uIx8l2xlYVY")
|
||||||
# It is also possible to specify a URL that includes the video ID:
|
|
||||||
# livechat = LiveChat("https://www.youtube.com/watch?v=Zvp1pJpie4I")
|
|
||||||
while livechat.is_alive():
|
|
||||||
try:
|
|
||||||
chatdata = livechat.get()
|
|
||||||
for c in chatdata.items:
|
|
||||||
print(f"{c.datetime} [{c.author.name}]- {c.message}")
|
|
||||||
chatdata.tick()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
livechat.terminate()
|
|
||||||
break
|
|
||||||
```
|
|
||||||
|
|
||||||
### callback mode
|
|
||||||
```python
|
|
||||||
from pytchat import LiveChat
|
|
||||||
import time
|
|
||||||
|
|
||||||
def main():
|
|
||||||
livechat = LiveChat(video_id = "Zvp1pJpie4I", callback = disp)
|
|
||||||
while livechat.is_alive():
|
|
||||||
#other background operation.
|
|
||||||
time.sleep(1)
|
|
||||||
livechat.terminate()
|
|
||||||
|
|
||||||
#callback function (automatically called)
|
|
||||||
def disp(chatdata):
|
|
||||||
for c in chatdata.items:
|
|
||||||
print(f"{c.datetime} [{c.author.name}]- {c.message}")
|
|
||||||
chatdata.tick()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### asyncio context:
|
|
||||||
```python
|
|
||||||
from pytchat import LiveChatAsync
|
|
||||||
from concurrent.futures import CancelledError
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
livechat = LiveChatAsync("Zvp1pJpie4I", callback = func)
|
|
||||||
while livechat.is_alive():
|
|
||||||
#other background operation.
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
#callback function is automatically called.
|
|
||||||
async def func(chatdata):
|
|
||||||
for c in chatdata.items:
|
|
||||||
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
|
||||||
await chatdata.tick_async()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
except CancelledError:
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### youtube api compatible processor:
|
|
||||||
```python
|
|
||||||
from pytchat import LiveChat, CompatibleProcessor
|
|
||||||
import time
|
|
||||||
|
|
||||||
chat = LiveChat("Zvp1pJpie4I",
|
|
||||||
processor = CompatibleProcessor() )
|
|
||||||
|
|
||||||
while chat.is_alive():
|
while chat.is_alive():
|
||||||
try:
|
for c in chat.get().sync_items():
|
||||||
data = chat.get()
|
print(f"{c.datetime} [{c.author.name}]- {c.message}")
|
||||||
polling = data['pollingIntervalMillis']/1000
|
|
||||||
for c in data['items']:
|
|
||||||
if c.get('snippet'):
|
|
||||||
print(f"[{c['authorDetails']['displayName']}]"
|
|
||||||
f"-{c['snippet']['displayMessage']}")
|
|
||||||
time.sleep(polling/len(data['items']))
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
chat.terminate()
|
|
||||||
```
|
```
|
||||||
### replay:
|
|
||||||
If specified video is not live,
|
|
||||||
automatically try to fetch archived chat data.
|
|
||||||
|
|
||||||
|
|
||||||
|
### Output JSON format string (feature of [DefaultProcessor](https://github.com/taizan-hokuto/pytchat/wiki/DefaultProcessor))
|
||||||
```python
|
```python
|
||||||
from pytchat import LiveChat
|
import pytchat
|
||||||
|
import time
|
||||||
|
|
||||||
def main():
|
chat = pytchat.create(video_id="uIx8l2xlYVY")
|
||||||
#seektime (seconds): start position of chat.
|
while chat.is_alive():
|
||||||
chat = LiveChat("ojes5ULOqhc", seektime = 60*30)
|
print(chat.get().json())
|
||||||
print('Replay from 30:00')
|
time.sleep(5)
|
||||||
try:
|
'''
|
||||||
while chat.is_alive():
|
# Each chat item can also be output in JSON format.
|
||||||
data = chat.get()
|
for c in chat.get().items:
|
||||||
for c in data.items:
|
print(c.json())
|
||||||
print(f"{c.elapsedTime} [{c.author.name}]-{c.message} {c.amountString}")
|
'''
|
||||||
data.tick()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
chat.terminate()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
```
|
```
|
||||||
### Extract archived chat data as [HTML](https://github.com/taizan-hokuto/pytchat/wiki/HTMLArchiver) or [tab separated values](https://github.com/taizan-hokuto/pytchat/wiki/TSVArchiver).
|
|
||||||
```python
|
|
||||||
from pytchat import HTMLArchiver, Extractor
|
|
||||||
|
|
||||||
video_id = "*******"
|
|
||||||
ex = Extractor(
|
|
||||||
video_id,
|
|
||||||
div=10,
|
|
||||||
processor=HTMLArchiver("c:/test.html")
|
|
||||||
)
|
|
||||||
|
|
||||||
ex.extract()
|
### other
|
||||||
print("finished.")
|
+ Fetch chat with a buffer ([LiveChat](https://github.com/taizan-hokuto/pytchat/wiki/LiveChat))
|
||||||
```
|
|
||||||
|
+ Use with asyncio ([LiveChatAsync](https://github.com/taizan-hokuto/pytchat/wiki/LiveChatAsync))
|
||||||
|
|
||||||
|
+ YT API compatible chat processor ([CompatibleProcessor](https://github.com/taizan-hokuto/pytchat/wiki/CompatibleProcessor))
|
||||||
|
|
||||||
|
|
||||||
## Structure of Default Processor
|
## Structure of Default Processor
|
||||||
Each item can be got with `items` function.
|
Each item can be got with `sync_items()` function.
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>name</th>
|
<th>name</th>
|
||||||
@@ -291,16 +188,3 @@ Structure of author object.
|
|||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
|
|
||||||
## Contributes
|
|
||||||
Great thanks:
|
|
||||||
|
|
||||||
Most of source code of CLI refer to:
|
|
||||||
|
|
||||||
[PetterKraabol / Twitch-Chat-Downloader](https://github.com/PetterKraabol/Twitch-Chat-Downloader)
|
|
||||||
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
[taizan-hokuto](https://github.com/taizan-hokuto)
|
|
||||||
|
|
||||||
[twitter:@taizan205](https://twitter.com/taizan205)
|
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
"""
|
"""
|
||||||
pytchat is a lightweight python library to browse youtube livechat without Selenium or BeautifulSoup.
|
pytchat is a lightweight python library to browse youtube livechat without Selenium or BeautifulSoup.
|
||||||
"""
|
"""
|
||||||
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
|
__copyright__ = 'Copyright (C) 2019, 2020, 2021 taizan-hokuto'
|
||||||
__version__ = '0.1.8'
|
__version__ = '0.5.5'
|
||||||
__license__ = 'MIT'
|
__license__ = 'MIT'
|
||||||
__author__ = 'taizan-hokuto'
|
__author__ = 'taizan-hokuto'
|
||||||
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
||||||
__url__ = 'https://github.com/taizan-hokuto/pytchat'
|
__url__ = 'https://github.com/taizan-hokuto/pytchat'
|
||||||
|
|
||||||
__all__ = ["core_async","core_multithread","processors"]
|
|
||||||
|
from .exceptions import (
|
||||||
|
ChatParseException,
|
||||||
|
ResponseContextError,
|
||||||
|
NoContents,
|
||||||
|
NoContinuation,
|
||||||
|
IllegalFunctionCall,
|
||||||
|
InvalidVideoIdException,
|
||||||
|
UnknownConnectionError,
|
||||||
|
RetryExceedMaxCount,
|
||||||
|
ChatDataFinished,
|
||||||
|
ReceivedUnknownContinuation,
|
||||||
|
FailedExtractContinuation,
|
||||||
|
VideoInfoParseError,
|
||||||
|
PatternUnmatchError
|
||||||
|
)
|
||||||
|
|
||||||
from .api import (
|
from .api import (
|
||||||
cli,
|
|
||||||
config,
|
config,
|
||||||
LiveChat,
|
LiveChat,
|
||||||
LiveChatAsync,
|
LiveChatAsync,
|
||||||
@@ -19,14 +33,12 @@ from .api import (
|
|||||||
CompatibleProcessor,
|
CompatibleProcessor,
|
||||||
DummyProcessor,
|
DummyProcessor,
|
||||||
DefaultProcessor,
|
DefaultProcessor,
|
||||||
Extractor,
|
|
||||||
HTMLArchiver,
|
HTMLArchiver,
|
||||||
TSVArchiver,
|
TSVArchiver,
|
||||||
JsonfileArchiver,
|
JsonfileArchiver,
|
||||||
SimpleDisplayProcessor,
|
SimpleDisplayProcessor,
|
||||||
SpeedCalculator,
|
SpeedCalculator,
|
||||||
SuperchatCalculator,
|
SuperchatCalculator,
|
||||||
VideoInfo
|
create
|
||||||
)
|
)
|
||||||
|
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from . import cli
|
|
||||||
from . import config
|
from . import config
|
||||||
|
from .core import create
|
||||||
from .core_multithread.livechat import LiveChat
|
from .core_multithread.livechat import LiveChat
|
||||||
from .core_async.livechat import LiveChatAsync
|
from .core_async.livechat import LiveChatAsync
|
||||||
from .processors.chat_processor import ChatProcessor
|
from .processors.chat_processor import ChatProcessor
|
||||||
@@ -12,7 +12,23 @@ from .processors.jsonfile_archiver import JsonfileArchiver
|
|||||||
from .processors.simple_display_processor import SimpleDisplayProcessor
|
from .processors.simple_display_processor import SimpleDisplayProcessor
|
||||||
from .processors.speed.calculator import SpeedCalculator
|
from .processors.speed.calculator import SpeedCalculator
|
||||||
from .processors.superchat.calculator import SuperchatCalculator
|
from .processors.superchat.calculator import SuperchatCalculator
|
||||||
from .tool.extract.extractor import Extractor
|
|
||||||
from .tool.videoinfo import VideoInfo
|
|
||||||
|
__all__ = [
|
||||||
|
config,
|
||||||
|
LiveChat,
|
||||||
|
LiveChatAsync,
|
||||||
|
ChatProcessor,
|
||||||
|
CompatibleProcessor,
|
||||||
|
DummyProcessor,
|
||||||
|
DefaultProcessor,
|
||||||
|
HTMLArchiver,
|
||||||
|
TSVArchiver,
|
||||||
|
JsonfileArchiver,
|
||||||
|
SimpleDisplayProcessor,
|
||||||
|
SpeedCalculator,
|
||||||
|
SuperchatCalculator,
|
||||||
|
create
|
||||||
|
]
|
||||||
|
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import argparse
|
|
||||||
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
from json.decoder import JSONDecodeError
|
|
||||||
from pathlib import Path
|
|
||||||
from .arguments import Arguments
|
|
||||||
from .progressbar import ProgressBar
|
|
||||||
from .. exceptions import InvalidVideoIdException, NoContents, PatternUnmatchError
|
|
||||||
from .. processors.html_archiver import HTMLArchiver
|
|
||||||
from .. tool.extract.extractor import Extractor
|
|
||||||
from .. tool.videoinfo import VideoInfo
|
|
||||||
from .. util.extract_video_id import extract_video_id
|
|
||||||
from .. import util
|
|
||||||
from .. import __version__
|
|
||||||
|
|
||||||
'''
|
|
||||||
Most of CLI modules refer to
|
|
||||||
Petter Kraabøl's Twitch-Chat-Downloader
|
|
||||||
https://github.com/PetterKraabol/Twitch-Chat-Downloader
|
|
||||||
(MIT License)
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Arguments
|
|
||||||
parser = argparse.ArgumentParser(description=f'pytchat v{__version__}')
|
|
||||||
parser.add_argument('-v', f'--{Arguments.Name.VIDEO_IDS}', type=str,
|
|
||||||
help='Video ID (or URL that includes Video ID). You can specify multiple video IDs by '
|
|
||||||
'separating them with commas without spaces.\n'
|
|
||||||
'If ID starts with a hyphen (-), enclose the ID in square brackets.')
|
|
||||||
parser.add_argument('-o', f'--{Arguments.Name.OUTPUT}', type=str,
|
|
||||||
help='Output directory (end with "/"). default="./"', default='./')
|
|
||||||
parser.add_argument(f'--{Arguments.Name.VERSION}', action='store_true',
|
|
||||||
help='Show version')
|
|
||||||
parser.add_argument(f'--{Arguments.Name.SAVE_ERROR_DATA}', action='store_true',
|
|
||||||
help='Save error data when error occurs(".dat" file)')
|
|
||||||
Arguments(parser.parse_args().__dict__)
|
|
||||||
if Arguments().print_version:
|
|
||||||
print(f'pytchat v{__version__} © 2019 taizan-hokuto')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Extractor
|
|
||||||
if not Arguments().video_ids:
|
|
||||||
parser.print_help()
|
|
||||||
return
|
|
||||||
for video_id in Arguments().video_ids:
|
|
||||||
if '[' in video_id:
|
|
||||||
video_id = video_id.replace('[', '').replace(']', '')
|
|
||||||
try:
|
|
||||||
video_id = extract_video_id(video_id)
|
|
||||||
if os.path.exists(Arguments().output):
|
|
||||||
path = Path(Arguments().output + video_id + '.html')
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError
|
|
||||||
info = VideoInfo(video_id)
|
|
||||||
print(f"Extracting...\n"
|
|
||||||
f" video_id: {video_id}\n"
|
|
||||||
f" channel: {info.get_channel_name()}\n"
|
|
||||||
f" title: {info.get_title()}")
|
|
||||||
|
|
||||||
print(f" output path: {path.resolve()}")
|
|
||||||
duration = info.get_duration()
|
|
||||||
pbar = ProgressBar(total=(duration * 1000) / 0.99, status="Extracting")
|
|
||||||
ex = Extractor(video_id,
|
|
||||||
callback=pbar._disp,
|
|
||||||
div=10)
|
|
||||||
signal.signal(signal.SIGINT, (lambda a, b: cancel(ex, pbar)))
|
|
||||||
data = ex.extract()
|
|
||||||
if data == []:
|
|
||||||
return False
|
|
||||||
pbar.reset("#", "=", total=len(data), status="Rendering ")
|
|
||||||
processor = HTMLArchiver(Arguments().output + video_id + '.html', callback=pbar._disp)
|
|
||||||
processor.process(
|
|
||||||
[{'video_id': None,
|
|
||||||
'timeout': 1,
|
|
||||||
'chatdata': (action["replayChatItemAction"]["actions"][0] for action in data)}]
|
|
||||||
)
|
|
||||||
processor.finalize()
|
|
||||||
pbar.reset('#', '#', status='Completed ')
|
|
||||||
pbar.close()
|
|
||||||
print()
|
|
||||||
if pbar.is_cancelled():
|
|
||||||
print("\nThe extraction process has been discontinued.\n")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
except InvalidVideoIdException:
|
|
||||||
print("Invalid Video ID or URL:", video_id)
|
|
||||||
except TypeError as e:
|
|
||||||
print(e.with_traceback())
|
|
||||||
except NoContents as e:
|
|
||||||
print(e)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("The specified directory does not exist.:{}".format(Arguments().output))
|
|
||||||
except JSONDecodeError as e:
|
|
||||||
print(e.msg)
|
|
||||||
print("Cannot parse video information.:{}".format(video_id))
|
|
||||||
if Arguments().save_error_data:
|
|
||||||
util.save(e.doc, "ERR_JSON_DECODE", ".dat")
|
|
||||||
except PatternUnmatchError as e:
|
|
||||||
print(e.msg)
|
|
||||||
print("Cannot parse video information.:{}".format(video_id))
|
|
||||||
if Arguments().save_error_data:
|
|
||||||
util.save(e.doc, "ERR_PATTERN_UNMATCH", ".dat")
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def cancel(ex: Extractor, pbar: ProgressBar):
|
|
||||||
ex.cancel()
|
|
||||||
pbar.cancel()
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
from typing import Optional, Dict, Union, List
|
|
||||||
from .singleton import Singleton
|
|
||||||
|
|
||||||
'''
|
|
||||||
This modules refer to
|
|
||||||
Petter Kraabøl's Twitch-Chat-Downloader
|
|
||||||
https://github.com/PetterKraabol/Twitch-Chat-Downloader
|
|
||||||
(MIT License)
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
class Arguments(metaclass=Singleton):
|
|
||||||
"""
|
|
||||||
Arguments singleton
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Name:
|
|
||||||
VERSION: str = 'version'
|
|
||||||
OUTPUT: str = 'output_dir'
|
|
||||||
VIDEO_IDS: str = 'video_id'
|
|
||||||
SAVE_ERROR_DATA: bool = 'save_error_data'
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
arguments: Optional[Dict[str, Union[str, bool, int]]] = None):
|
|
||||||
"""
|
|
||||||
Initialize arguments
|
|
||||||
:param arguments: Arguments from cli
|
|
||||||
(Optional to call singleton instance without parameters)
|
|
||||||
"""
|
|
||||||
|
|
||||||
if arguments is None:
|
|
||||||
print('Error: arguments were not provided')
|
|
||||||
exit()
|
|
||||||
|
|
||||||
self.print_version: bool = arguments[Arguments.Name.VERSION]
|
|
||||||
self.output: str = arguments[Arguments.Name.OUTPUT]
|
|
||||||
self.video_ids: List[int] = []
|
|
||||||
self.save_error_data: bool = arguments[Arguments.Name.SAVE_ERROR_DATA]
|
|
||||||
# Videos
|
|
||||||
if arguments[Arguments.Name.VIDEO_IDS]:
|
|
||||||
self.video_ids = [video_id
|
|
||||||
for video_id in arguments[Arguments.Name.VIDEO_IDS].split(',')]
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
'''
|
|
||||||
This code for this progress bar is based on
|
|
||||||
vladignatyev/progress.py
|
|
||||||
https://gist.github.com/vladignatyev/06860ec2040cb497f0f3
|
|
||||||
(MIT License)
|
|
||||||
'''
|
|
||||||
import sys
|
|
||||||
|
|
||||||
ROT = ['\u25F4', '\u25F5', '\u25F6', '\u25F7']
|
|
||||||
|
|
||||||
|
|
||||||
class ProgressBar:
|
|
||||||
def __init__(self, total, status):
|
|
||||||
self._bar_len = 60
|
|
||||||
self._cancelled = False
|
|
||||||
self.reset(total=total, status=status)
|
|
||||||
self._blinker = 0
|
|
||||||
|
|
||||||
def reset(self, symbol_done="=", symbol_space=" ", total=100, status=''):
|
|
||||||
self._symbol_done = symbol_done
|
|
||||||
self._symbol_space = symbol_space
|
|
||||||
self._total = total
|
|
||||||
self._status = status
|
|
||||||
self._count = 0
|
|
||||||
|
|
||||||
def _disp(self, _, fetched):
|
|
||||||
self._progress(fetched, self._total)
|
|
||||||
|
|
||||||
def _progress(self, fillin, total):
|
|
||||||
if total == 0 or self._cancelled:
|
|
||||||
return
|
|
||||||
self._count += fillin
|
|
||||||
filled_len = int(round(self._bar_len * self._count / float(total)))
|
|
||||||
percents = round(100.0 * self._count / float(total), 1)
|
|
||||||
if percents > 100:
|
|
||||||
percents = 100.0
|
|
||||||
if filled_len > self._bar_len:
|
|
||||||
filled_len = self._bar_len
|
|
||||||
|
|
||||||
bar = self._symbol_done * filled_len + \
|
|
||||||
self._symbol_space * (self._bar_len - filled_len)
|
|
||||||
sys.stdout.write(' [%s] %s%s ...%s %s \r' % (bar, percents, '%', self._status, ROT[self._blinker % 4]))
|
|
||||||
sys.stdout.flush()
|
|
||||||
self._blinker += 1
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if not self._cancelled:
|
|
||||||
self._progress(self._total, self._total)
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
self._cancelled = True
|
|
||||||
|
|
||||||
def is_cancelled(self):
|
|
||||||
return self._cancelled
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
'''
|
|
||||||
This modules refer to
|
|
||||||
Petter Kraabøl's Twitch-Chat-Downloader
|
|
||||||
https://github.com/PetterKraabol/Twitch-Chat-Downloader
|
|
||||||
(MIT License)
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
class Singleton(type):
|
|
||||||
"""
|
|
||||||
Abstract class for singletons
|
|
||||||
"""
|
|
||||||
_instances = {}
|
|
||||||
|
|
||||||
def __call__(cls, *args, **kwargs):
|
|
||||||
if cls not in cls._instances:
|
|
||||||
cls._instances[cls] = super().__call__(*args, **kwargs)
|
|
||||||
return cls._instances[cls]
|
|
||||||
|
|
||||||
def get_instance(cls, *args, **kwargs):
|
|
||||||
cls.__call__(*args, **kwargs)
|
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import logging
|
import logging # noqa
|
||||||
from . import mylogger
|
from . import mylogger
|
||||||
|
from base64 import a85decode as dc
|
||||||
headers = {
|
headers = {
|
||||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36',
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36 Edg/86.0.622.63,gzip(gfe)',
|
||||||
}
|
}
|
||||||
|
m_headers = {
|
||||||
|
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36 Edg/91.0.864.59',
|
||||||
|
}
|
||||||
|
_sml = dc(b"BQS?8F#ks-GB\\6`H#IhIF^eo7@rH3;H#IhIF^eor06T''Ch\\'(?XmbXF>%9<FC/iuG%G#jBOQ!ICLqcS5tQB2;gCZ)?UdXC;f$GR3)MM2<(0>O7mh!,G@+K5?SO9T@okV").decode()
|
||||||
|
_smr = dc(b"BQS?8F#ks-GB\\6`H#IhIF^eo7@rH3;H#IhIF^eor06T''Ch\\'(?XmbXF>%9<FC/iuG%G#jBOQ!iEb03+@<k(QAU-F)8U=fDGsP557S5F7CiNH7;)D3N77^*B6YU@\\?WfBr0emZX=#^").decode()
|
||||||
|
|
||||||
|
|
||||||
def logger(module_name: str, loglevel=None):
|
def logger(module_name: str, loglevel=None):
|
||||||
|
|||||||
7
pytchat/core/__init__.py
Normal file
7
pytchat/core/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from .pytchat import PytchatCore
|
||||||
|
from .. util import extract_video_id
|
||||||
|
|
||||||
|
|
||||||
|
def create(video_id: str, **kwargs):
|
||||||
|
_vid = extract_video_id(video_id)
|
||||||
|
return PytchatCore(_vid, **kwargs)
|
||||||
222
pytchat/core/pytchat.py
Normal file
222
pytchat/core/pytchat.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from ..parser.live import Parser
|
||||||
|
from .. import config
|
||||||
|
from .. import exceptions
|
||||||
|
from ..paramgen import liveparam, arcparam
|
||||||
|
from ..processors.default.processor import DefaultProcessor
|
||||||
|
from ..processors.combinator import Combinator
|
||||||
|
from .. import util
|
||||||
|
|
||||||
|
headers = config.headers
|
||||||
|
MAX_RETRY = 10
|
||||||
|
|
||||||
|
class PytchatCore:
|
||||||
|
'''
|
||||||
|
|
||||||
|
Parameter
|
||||||
|
---------
|
||||||
|
video_id : str
|
||||||
|
|
||||||
|
seektime : int
|
||||||
|
start position of fetching chat (seconds).
|
||||||
|
This option is valid for archived chat only.
|
||||||
|
If negative value, chat data posted before the start of the broadcast
|
||||||
|
will be retrieved as well.
|
||||||
|
|
||||||
|
processor : ChatProcessor
|
||||||
|
|
||||||
|
client : httpx.Client
|
||||||
|
The client for connecting youtube.
|
||||||
|
You can specify any customized httpx client (e.g. coolies, user agent).
|
||||||
|
|
||||||
|
interruptable : bool
|
||||||
|
Allows keyboard interrupts.
|
||||||
|
Set this parameter to False if your own multi-threading program causes
|
||||||
|
the problem.
|
||||||
|
|
||||||
|
force_replay : bool
|
||||||
|
force to fetch archived chat data, even if specified video is live.
|
||||||
|
|
||||||
|
topchat_only : bool
|
||||||
|
If True, get only top chat.
|
||||||
|
|
||||||
|
hold_exception : bool [default:True]
|
||||||
|
If True, when exceptions occur, the exception is held internally,
|
||||||
|
and can be raised by raise_for_status().
|
||||||
|
|
||||||
|
replay_continuation : str
|
||||||
|
If this parameter is not None, the processor will attempt to get chat data from continuation.
|
||||||
|
This parameter is only allowed in archived mode.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
---------
|
||||||
|
_is_alive : bool
|
||||||
|
Flag to stop getting chat.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, video_id,
|
||||||
|
seektime=-1,
|
||||||
|
processor=DefaultProcessor(),
|
||||||
|
client = httpx.Client(http2=True),
|
||||||
|
interruptable=True,
|
||||||
|
force_replay=False,
|
||||||
|
topchat_only=False,
|
||||||
|
hold_exception=True,
|
||||||
|
logger=config.logger(__name__),
|
||||||
|
replay_continuation=None
|
||||||
|
):
|
||||||
|
self._client = client
|
||||||
|
self._video_id = util.extract_video_id(video_id)
|
||||||
|
self.seektime = seektime
|
||||||
|
if isinstance(processor, tuple):
|
||||||
|
self.processor = Combinator(processor)
|
||||||
|
else:
|
||||||
|
self.processor = processor
|
||||||
|
self._is_alive = True
|
||||||
|
self._is_replay = force_replay or (replay_continuation is not None)
|
||||||
|
self._hold_exception = hold_exception
|
||||||
|
self._exception_holder = None
|
||||||
|
self._parser = Parser(
|
||||||
|
is_replay=self._is_replay,
|
||||||
|
exception_holder=self._exception_holder
|
||||||
|
)
|
||||||
|
self._first_fetch = replay_continuation is None
|
||||||
|
self._fetch_url = config._sml if replay_continuation is None else config._smr
|
||||||
|
self._topchat_only = topchat_only
|
||||||
|
self._dat = ''
|
||||||
|
self._last_offset_ms = 0
|
||||||
|
self._logger = logger
|
||||||
|
self.continuation = replay_continuation
|
||||||
|
if interruptable:
|
||||||
|
signal.signal(signal.SIGINT, lambda a, b: self.terminate())
|
||||||
|
self._setup()
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
if not self.continuation:
|
||||||
|
time.sleep(0.1) # sleep shortly to prohibit skipping fetching data
|
||||||
|
"""Fetch first continuation parameter,
|
||||||
|
create and start _listen loop.
|
||||||
|
"""
|
||||||
|
self.continuation = liveparam.getparam(
|
||||||
|
self._video_id,
|
||||||
|
channel_id=util.get_channelid(self._client, self._video_id),
|
||||||
|
past_sec=3)
|
||||||
|
|
||||||
|
def _get_chat_component(self):
|
||||||
|
''' Fetch chat data and store them into buffer,
|
||||||
|
get next continuaiton parameter and loop.
|
||||||
|
|
||||||
|
Parameter
|
||||||
|
---------
|
||||||
|
continuation : str
|
||||||
|
parameter for next chat data
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if self.continuation and self._is_alive:
|
||||||
|
contents = self._get_contents(self.continuation, self._client, headers)
|
||||||
|
metadata, chatdata = self._parser.parse(contents)
|
||||||
|
timeout = metadata['timeoutMs'] / 1000
|
||||||
|
chat_component = {
|
||||||
|
"video_id": self._video_id,
|
||||||
|
"timeout": timeout,
|
||||||
|
"chatdata": chatdata
|
||||||
|
}
|
||||||
|
self.continuation = metadata.get('continuation')
|
||||||
|
self._last_offset_ms = metadata.get('last_offset_ms', 0)
|
||||||
|
return chat_component
|
||||||
|
except exceptions.ChatParseException as e:
|
||||||
|
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
||||||
|
self._raise_exception(e)
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"{traceback.format_exc(limit=-1)}")
|
||||||
|
self._raise_exception(e)
|
||||||
|
|
||||||
|
def _get_contents(self, continuation, client, headers):
|
||||||
|
'''Get 'continuationContents' from livechat json.
|
||||||
|
If contents is None at first fetching,
|
||||||
|
try to fetch archive chat data.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
-------
|
||||||
|
'continuationContents' which includes metadata & chat data.
|
||||||
|
'''
|
||||||
|
livechat_json = self._get_livechat_json(
|
||||||
|
continuation, client, replay=self._is_replay, offset_ms=self._last_offset_ms)
|
||||||
|
contents, dat = self._parser.get_contents(livechat_json)
|
||||||
|
if self._dat == '' and dat:
|
||||||
|
self._dat = dat
|
||||||
|
if self._first_fetch:
|
||||||
|
if contents is None or self._is_replay:
|
||||||
|
'''Try to fetch archive chat data.'''
|
||||||
|
self._parser.is_replay = True
|
||||||
|
self._fetch_url = config._smr
|
||||||
|
continuation = arcparam.getparam(
|
||||||
|
self._video_id, self.seektime, self._topchat_only, util.get_channelid(client, self._video_id))
|
||||||
|
livechat_json = self._get_livechat_json(
|
||||||
|
continuation, client, replay=True, offset_ms=self.seektime * 1000)
|
||||||
|
reload_continuation = self._parser.reload_continuation(
|
||||||
|
self._parser.get_contents(livechat_json)[0])
|
||||||
|
if reload_continuation:
|
||||||
|
livechat_json = (self._get_livechat_json(
|
||||||
|
reload_continuation, client, headers))
|
||||||
|
contents, _ = self._parser.get_contents(livechat_json)
|
||||||
|
self._is_replay = True
|
||||||
|
self._first_fetch = False
|
||||||
|
return contents
|
||||||
|
|
||||||
|
def _get_livechat_json(self, continuation, client, replay: bool, offset_ms: int = 0):
|
||||||
|
'''
|
||||||
|
Get json which includes chat data.
|
||||||
|
'''
|
||||||
|
livechat_json = None
|
||||||
|
err = None
|
||||||
|
if offset_ms < 0:
|
||||||
|
offset_ms = 0
|
||||||
|
param = util.get_param(continuation, dat=self._dat, replay=replay, offsetms=offset_ms)
|
||||||
|
for _ in range(MAX_RETRY + 1):
|
||||||
|
try:
|
||||||
|
response = client.post(self._fetch_url, json=param)
|
||||||
|
livechat_json = response.json()
|
||||||
|
break
|
||||||
|
except (json.JSONDecodeError, httpx.ConnectTimeout, httpx.ReadTimeout, httpx.ConnectError) as e:
|
||||||
|
err = e
|
||||||
|
time.sleep(2)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self._logger.error(f"[{self._video_id}]"
|
||||||
|
f"Exceeded retry count. Last error: {str(err)}")
|
||||||
|
self._raise_exception(exceptions.RetryExceedMaxCount())
|
||||||
|
return livechat_json
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
if self.is_alive():
|
||||||
|
chat_component = self._get_chat_component()
|
||||||
|
return self.processor.process([chat_component])
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def is_replay(self):
|
||||||
|
return self._is_replay
|
||||||
|
|
||||||
|
def is_alive(self):
|
||||||
|
return self._is_alive
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
if not self.is_alive():
|
||||||
|
return
|
||||||
|
self._is_alive = False
|
||||||
|
self.processor.finalize()
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
if self._exception_holder is not None:
|
||||||
|
raise self._exception_holder
|
||||||
|
|
||||||
|
def _raise_exception(self, exception: Exception = None):
|
||||||
|
self.terminate()
|
||||||
|
if self._hold_exception is False:
|
||||||
|
raise exception
|
||||||
|
self._exception_holder = exception
|
||||||
@@ -4,13 +4,13 @@ import asyncio
|
|||||||
|
|
||||||
class Buffer(asyncio.Queue):
|
class Buffer(asyncio.Queue):
|
||||||
'''
|
'''
|
||||||
チャットデータを格納するバッファの役割を持つFIFOキュー
|
Buffer for storing chat data.
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
maxsize : int
|
maxsize : int
|
||||||
格納するチャットブロックの最大個数。0の場合は無限。
|
Maximum number of chat blocks to be stored.
|
||||||
最大値を超える場合は古いチャットブロックから破棄される。
|
If it exceeds the maximum, the oldest chat block will be discarded.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, maxsize=0):
|
def __init__(self, maxsize=0):
|
||||||
|
|||||||
@@ -5,71 +5,71 @@ import json
|
|||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
|
||||||
from asyncio import Queue
|
from asyncio import Queue
|
||||||
from concurrent.futures import CancelledError
|
from concurrent.futures import CancelledError
|
||||||
from .buffer import Buffer
|
from .buffer import Buffer
|
||||||
from ..parser.live import Parser
|
from ..parser.live import Parser
|
||||||
from .. import config
|
from .. import config
|
||||||
from .. import exceptions
|
from .. import exceptions
|
||||||
|
from .. import util
|
||||||
from ..paramgen import liveparam, arcparam
|
from ..paramgen import liveparam, arcparam
|
||||||
from ..processors.default.processor import DefaultProcessor
|
from ..processors.default.processor import DefaultProcessor
|
||||||
from ..processors.combinator import Combinator
|
from ..processors.combinator import Combinator
|
||||||
from ..util.extract_video_id import extract_video_id
|
|
||||||
|
|
||||||
headers = config.headers
|
headers = config.headers
|
||||||
MAX_RETRY = 10
|
MAX_RETRY = 10
|
||||||
|
|
||||||
|
|
||||||
class LiveChatAsync:
|
class LiveChatAsync:
|
||||||
'''asyncioを利用してYouTubeのライブ配信のチャットデータを取得する。
|
'''LiveChatAsync object fetches chat data and stores them
|
||||||
|
in a buffer with asyncio.
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
video_id : str
|
video_id : str
|
||||||
動画ID
|
|
||||||
|
|
||||||
seektime : int
|
seektime : int
|
||||||
(ライブチャット取得時は無視)
|
start position of fetching chat (seconds).
|
||||||
取得開始するアーカイブ済みチャットの経過時間(秒)
|
This option is valid for archived chat only.
|
||||||
マイナス値を指定した場合は、配信開始前のチャットも取得する。
|
If negative value, chat data posted before the start of the broadcast
|
||||||
|
will be retrieved as well.
|
||||||
|
|
||||||
processor : ChatProcessor
|
processor : ChatProcessor
|
||||||
チャットデータを加工するオブジェクト
|
|
||||||
|
|
||||||
buffer : Buffer(maxsize:20[default])
|
buffer : Buffer
|
||||||
チャットデータchat_componentを格納するバッファ。
|
buffer of chat data fetched background.
|
||||||
maxsize : 格納できるchat_componentの個数
|
|
||||||
default値20個。1個で約5~10秒分。
|
|
||||||
|
|
||||||
interruptable : bool
|
interruptable : bool
|
||||||
Ctrl+Cによる処理中断を行うかどうか。
|
Allows keyboard interrupts.
|
||||||
|
Set this parameter to False if your own threading program causes
|
||||||
|
the problem.
|
||||||
|
|
||||||
callback : func
|
callback : func
|
||||||
_listen()関数から一定間隔で自動的に呼びだす関数。
|
function called periodically from _listen().
|
||||||
|
|
||||||
done_callback : func
|
done_callback : func
|
||||||
listener終了時に呼び出すコールバック。
|
function called when listener ends.
|
||||||
|
|
||||||
exception_handler : func
|
exception_handler : func
|
||||||
例外を処理する関数
|
|
||||||
|
|
||||||
direct_mode : bool
|
direct_mode : bool
|
||||||
Trueの場合、bufferを使わずにcallbackを呼ぶ。
|
If True, invoke specified callback function without using buffer.
|
||||||
Trueの場合、callbackの設定が必須
|
callback is required. If not, IllegalFunctionCall will be raised.
|
||||||
(設定していない場合IllegalFunctionCall例外を発生させる)
|
|
||||||
|
|
||||||
force_replay : bool
|
force_replay : bool
|
||||||
Trueの場合、ライブチャットが取得できる場合であっても
|
force to fetch archived chat data, even if specified video is live.
|
||||||
強制的にアーカイブ済みチャットを取得する。
|
|
||||||
|
|
||||||
topchat_only : bool
|
topchat_only : bool
|
||||||
Trueの場合、上位チャットのみ取得する。
|
If True, get only top chat.
|
||||||
|
|
||||||
|
replay_continuation : str
|
||||||
|
If this parameter is not None, the processor will attempt to get chat data from continuation.
|
||||||
|
This parameter is only allowed in archived mode.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
---------
|
---------
|
||||||
_is_alive : bool
|
_is_alive : bool
|
||||||
チャット取得を停止するためのフラグ
|
Flag to stop getting chat.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_setup_finished = False
|
_setup_finished = False
|
||||||
@@ -78,6 +78,7 @@ class LiveChatAsync:
|
|||||||
seektime=-1,
|
seektime=-1,
|
||||||
processor=DefaultProcessor(),
|
processor=DefaultProcessor(),
|
||||||
buffer=None,
|
buffer=None,
|
||||||
|
client = httpx.AsyncClient(http2=True),
|
||||||
interruptable=True,
|
interruptable=True,
|
||||||
callback=None,
|
callback=None,
|
||||||
done_callback=None,
|
done_callback=None,
|
||||||
@@ -86,8 +87,10 @@ class LiveChatAsync:
|
|||||||
force_replay=False,
|
force_replay=False,
|
||||||
topchat_only=False,
|
topchat_only=False,
|
||||||
logger=config.logger(__name__),
|
logger=config.logger(__name__),
|
||||||
|
replay_continuation=None
|
||||||
):
|
):
|
||||||
self._video_id = extract_video_id(video_id)
|
self._client:httpx.AsyncClient = client
|
||||||
|
self._video_id = util.extract_video_id(video_id)
|
||||||
self.seektime = seektime
|
self.seektime = seektime
|
||||||
if isinstance(processor, tuple):
|
if isinstance(processor, tuple):
|
||||||
self.processor = Combinator(processor)
|
self.processor = Combinator(processor)
|
||||||
@@ -99,46 +102,48 @@ class LiveChatAsync:
|
|||||||
self._exception_handler = exception_handler
|
self._exception_handler = exception_handler
|
||||||
self._direct_mode = direct_mode
|
self._direct_mode = direct_mode
|
||||||
self._is_alive = True
|
self._is_alive = True
|
||||||
self._is_replay = force_replay
|
self._is_replay = force_replay or (replay_continuation is not None)
|
||||||
self._parser = Parser(is_replay=self._is_replay)
|
self._parser = Parser(is_replay=self._is_replay)
|
||||||
self._pauser = Queue()
|
self._pauser = Queue()
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
self._first_fetch = True
|
self._first_fetch = replay_continuation is None
|
||||||
self._fetch_url = "live_chat/get_live_chat?continuation="
|
self._fetch_url = config._sml if replay_continuation is None else config._smr
|
||||||
self._topchat_only = topchat_only
|
self._topchat_only = topchat_only
|
||||||
|
self._dat = ''
|
||||||
|
self._last_offset_ms = 0
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
self.exception = None
|
self.exception = None
|
||||||
|
self.continuation = replay_continuation
|
||||||
LiveChatAsync._logger = logger
|
LiveChatAsync._logger = logger
|
||||||
|
|
||||||
if exception_handler:
|
if exception_handler:
|
||||||
self._set_exception_handler(exception_handler)
|
self._set_exception_handler(exception_handler)
|
||||||
if interruptable:
|
if interruptable:
|
||||||
signal.signal(signal.SIGINT,
|
signal.signal(signal.SIGINT,
|
||||||
(lambda a, b: asyncio.create_task(
|
(lambda a, b: self._keyboard_interrupt()))
|
||||||
LiveChatAsync.shutdown(None, signal.SIGINT, b))))
|
|
||||||
self._setup()
|
self._setup()
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
# direct modeがTrueでcallback未設定の場合例外発生。
|
# An exception is raised when direct mode is true and no callback is set.
|
||||||
if self._direct_mode:
|
if self._direct_mode:
|
||||||
if self._callback is None:
|
if self._callback is None:
|
||||||
raise exceptions.IllegalFunctionCall(
|
raise exceptions.IllegalFunctionCall(
|
||||||
"When direct_mode=True, callback parameter is required.")
|
"When direct_mode=True, callback parameter is required.")
|
||||||
else:
|
else:
|
||||||
# direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
|
# Create a default buffer if `direct_mode` is False and buffer is not set.
|
||||||
if self._buffer is None:
|
if self._buffer is None:
|
||||||
self._buffer = Buffer(maxsize=20)
|
self._buffer = Buffer(maxsize=20)
|
||||||
# callbackが指定されている場合はcallbackを呼ぶループタスクを作成
|
# Create a loop task to call callback if the `callback` param is specified.
|
||||||
if self._callback is None:
|
if self._callback is None:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# callbackを呼ぶループタスクの開始
|
# Create a loop task to call callback if the `callback` param is specified.
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.create_task(self._callback_loop(self._callback))
|
loop.create_task(self._callback_loop(self._callback))
|
||||||
# _listenループタスクの開始
|
# Start a loop task for _listen()
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
self.listen_task = loop.create_task(self._startlisten())
|
self.listen_task = loop.create_task(self._startlisten())
|
||||||
# add_done_callbackの登録
|
# Register add_done_callback
|
||||||
if self._done_callback is None:
|
if self._done_callback is None:
|
||||||
self.listen_task.add_done_callback(self._finish)
|
self.listen_task.add_done_callback(self._finish)
|
||||||
else:
|
else:
|
||||||
@@ -148,8 +153,14 @@ class LiveChatAsync:
|
|||||||
"""Fetch first continuation parameter,
|
"""Fetch first continuation parameter,
|
||||||
create and start _listen loop.
|
create and start _listen loop.
|
||||||
"""
|
"""
|
||||||
initial_continuation = liveparam.getparam(self._video_id, 3)
|
if not self.continuation:
|
||||||
await self._listen(initial_continuation)
|
channel_id = await util.get_channelid_async(self._client, self._video_id)
|
||||||
|
self.continuation = liveparam.getparam(
|
||||||
|
self._video_id,
|
||||||
|
channel_id,
|
||||||
|
past_sec=3)
|
||||||
|
|
||||||
|
await self._listen(self.continuation)
|
||||||
|
|
||||||
async def _listen(self, continuation):
|
async def _listen(self, continuation):
|
||||||
''' Fetch chat data and store them into buffer,
|
''' Fetch chat data and store them into buffer,
|
||||||
@@ -161,13 +172,14 @@ class LiveChatAsync:
|
|||||||
parameter for next chat data
|
parameter for next chat data
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(http2=True) as client:
|
async with self._client as client:
|
||||||
while(continuation and self._is_alive):
|
while(continuation and self._is_alive):
|
||||||
continuation = await self._check_pause(continuation)
|
continuation = await self._check_pause(continuation)
|
||||||
contents = await self._get_contents(
|
contents = await self._get_contents(continuation, client, headers) #Q#
|
||||||
continuation, client, headers)
|
|
||||||
metadata, chatdata = self._parser.parse(contents)
|
metadata, chatdata = self._parser.parse(contents)
|
||||||
|
continuation = metadata.get('continuation')
|
||||||
|
if continuation:
|
||||||
|
self.continuation = continuation
|
||||||
timeout = metadata['timeoutMs'] / 1000
|
timeout = metadata['timeoutMs'] / 1000
|
||||||
chat_component = {
|
chat_component = {
|
||||||
"video_id": self._video_id,
|
"video_id": self._video_id,
|
||||||
@@ -186,16 +198,15 @@ class LiveChatAsync:
|
|||||||
await self._buffer.put(chat_component)
|
await self._buffer.put(chat_component)
|
||||||
diff_time = timeout - (time.time() - time_mark)
|
diff_time = timeout - (time.time() - time_mark)
|
||||||
await asyncio.sleep(diff_time)
|
await asyncio.sleep(diff_time)
|
||||||
continuation = metadata.get('continuation')
|
self._last_offset_ms = metadata.get('last_offset_ms', 0)
|
||||||
except exceptions.ChatParseException as e:
|
except exceptions.ChatParseException as e:
|
||||||
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
||||||
raise
|
raise
|
||||||
except (TypeError, json.JSONDecodeError):
|
except Exception:
|
||||||
self._logger.error(f"{traceback.format_exc(limit = -1)}")
|
self._logger.error(f"{traceback.format_exc(limit=-1)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
self._logger.debug(f"[{self._video_id}]finished fetching chat.")
|
self._logger.debug(f"[{self._video_id}] finished fetching chat.")
|
||||||
raise exceptions.ChatDataFinished
|
|
||||||
|
|
||||||
async def _check_pause(self, continuation):
|
async def _check_pause(self, continuation):
|
||||||
if self._pauser.empty():
|
if self._pauser.empty():
|
||||||
@@ -206,8 +217,12 @@ class LiveChatAsync:
|
|||||||
'''
|
'''
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
if not self._is_replay:
|
if not self._is_replay:
|
||||||
continuation = liveparam.getparam(
|
async with self._client as client:
|
||||||
self._video_id, 3, self._topchat_only)
|
channel_id = await util.get_channelid_async(client, self.video_id)
|
||||||
|
continuation = liveparam.getparam(self._video_id,
|
||||||
|
channel_id,
|
||||||
|
past_sec=3)
|
||||||
|
|
||||||
return continuation
|
return continuation
|
||||||
|
|
||||||
async def _get_contents(self, continuation, client, headers):
|
async def _get_contents(self, continuation, client, headers):
|
||||||
@@ -219,57 +234,61 @@ class LiveChatAsync:
|
|||||||
-------
|
-------
|
||||||
'continuationContents' which includes metadata & chatdata.
|
'continuationContents' which includes metadata & chatdata.
|
||||||
'''
|
'''
|
||||||
livechat_json = await self._get_livechat_json(continuation, client, headers)
|
livechat_json = await self._get_livechat_json(continuation, client, replay=self._is_replay, offset_ms=self._last_offset_ms)
|
||||||
contents = self._parser.get_contents(livechat_json)
|
contents, dat = self._parser.get_contents(livechat_json)
|
||||||
|
if self._dat == '' and dat:
|
||||||
|
self._dat = dat
|
||||||
if self._first_fetch:
|
if self._first_fetch:
|
||||||
if contents is None or self._is_replay:
|
if contents is None or self._is_replay:
|
||||||
'''Try to fetch archive chat data.'''
|
'''Try to fetch archive chat data.'''
|
||||||
self._parser.is_replay = True
|
self._parser.is_replay = True
|
||||||
self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation="
|
self._fetch_url = config._smr
|
||||||
|
channelid = await util.get_channelid_async(client, self._video_id)
|
||||||
continuation = arcparam.getparam(
|
continuation = arcparam.getparam(
|
||||||
self._video_id, self.seektime, self._topchat_only)
|
self._video_id, self.seektime, self._topchat_only, channelid)
|
||||||
livechat_json = (await self._get_livechat_json(
|
livechat_json = (await self._get_livechat_json(
|
||||||
continuation, client, headers))
|
continuation, client, replay=True, offset_ms=self.seektime * 1000))
|
||||||
reload_continuation = self._parser.reload_continuation(
|
reload_continuation = self._parser.reload_continuation(
|
||||||
self._parser.get_contents(livechat_json))
|
self._parser.get_contents(livechat_json)[0])
|
||||||
if reload_continuation:
|
if reload_continuation:
|
||||||
livechat_json = (await self._get_livechat_json(
|
livechat_json = (await self._get_livechat_json(
|
||||||
reload_continuation, client, headers))
|
reload_continuation, client, headers))
|
||||||
contents = self._parser.get_contents(livechat_json)
|
contents, _ = self._parser.get_contents(livechat_json)
|
||||||
self._is_replay = True
|
self._is_replay = True
|
||||||
self._first_fetch = False
|
self._first_fetch = False
|
||||||
return contents
|
return contents
|
||||||
|
|
||||||
async def _get_livechat_json(self, continuation, client, headers):
|
async def _get_livechat_json(self, continuation, client, replay: bool, offset_ms: int = 0):
|
||||||
'''
|
'''
|
||||||
Get json which includes chat data.
|
Get json which includes chat data.
|
||||||
'''
|
'''
|
||||||
continuation = urllib.parse.quote(continuation)
|
|
||||||
livechat_json = None
|
livechat_json = None
|
||||||
status_code = 0
|
if offset_ms < 0:
|
||||||
url = f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1"
|
offset_ms = 0
|
||||||
|
param = util.get_param(continuation, dat=self._dat, replay=replay, offsetms=offset_ms)
|
||||||
for _ in range(MAX_RETRY + 1):
|
for _ in range(MAX_RETRY + 1):
|
||||||
try:
|
try:
|
||||||
resp = await client.get(url, headers=headers)
|
resp = await client.post(self._fetch_url, json=param)
|
||||||
livechat_json = resp.json()
|
livechat_json = resp.json()
|
||||||
break
|
break
|
||||||
except (httpx.HTTPError, json.JSONDecodeError):
|
except (json.JSONDecodeError, httpx.HTTPError):
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(2)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self._logger.error(f"[{self._video_id}]"
|
self._logger.error(f"[{self._video_id}]"
|
||||||
f"Exceeded retry count. status_code={status_code}")
|
f"Exceeded retry count.")
|
||||||
return None
|
raise exceptions.RetryExceedMaxCount()
|
||||||
return livechat_json
|
return livechat_json
|
||||||
|
|
||||||
async def _callback_loop(self, callback):
|
async def _callback_loop(self, callback):
|
||||||
""" コンストラクタでcallbackを指定している場合、バックグラウンドで
|
""" If a callback is specified in the constructor,
|
||||||
callbackに指定された関数に一定間隔でチャットデータを投げる。
|
it throws chat data at regular intervals to the
|
||||||
|
function specified in the callback in the backgroun
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
callback : func
|
callback : func
|
||||||
加工済みのチャットデータを渡す先の関数。
|
function to which the processed chat data is passed.
|
||||||
"""
|
"""
|
||||||
while self.is_alive():
|
while self.is_alive():
|
||||||
items = await self._buffer.get()
|
items = await self._buffer.get()
|
||||||
@@ -280,11 +299,13 @@ class LiveChatAsync:
|
|||||||
await self._callback(processed_chat)
|
await self._callback(processed_chat)
|
||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
""" bufferからデータを取り出し、processorに投げ、
|
"""
|
||||||
加工済みのチャットデータを返す。
|
Retrieves data from the buffer,
|
||||||
|
throws it to the processor,
|
||||||
|
and returns the processed chat data.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
: Processorによって加工されたチャットデータ
|
: Chat data processed by the Processor
|
||||||
"""
|
"""
|
||||||
if self._callback is None:
|
if self._callback is None:
|
||||||
if self.is_alive():
|
if self.is_alive():
|
||||||
@@ -293,7 +314,7 @@ class LiveChatAsync:
|
|||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
raise exceptions.IllegalFunctionCall(
|
raise exceptions.IllegalFunctionCall(
|
||||||
"既にcallbackを登録済みのため、get()は実行できません。")
|
"Callback parameter is already set, so get() cannot be performed.")
|
||||||
|
|
||||||
def is_replay(self):
|
def is_replay(self):
|
||||||
return self._is_replay
|
return self._is_replay
|
||||||
@@ -314,23 +335,26 @@ class LiveChatAsync:
|
|||||||
return self._is_alive
|
return self._is_alive
|
||||||
|
|
||||||
def _finish(self, sender):
|
def _finish(self, sender):
|
||||||
'''Listener終了時のコールバック'''
|
'''Called when the _listen() task finished.'''
|
||||||
try:
|
try:
|
||||||
self._task_finished()
|
self._task_finished()
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
self._logger.debug(f'[{self._video_id}]cancelled:{sender}')
|
self._logger.debug(f'[{self._video_id}] cancelled:{sender}')
|
||||||
|
|
||||||
def terminate(self):
|
def terminate(self):
|
||||||
|
if not self.is_alive():
|
||||||
|
return
|
||||||
if self._pauser.empty():
|
if self._pauser.empty():
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
self._is_alive = False
|
self._is_alive = False
|
||||||
self._buffer.put_nowait({})
|
self._buffer.put_nowait({})
|
||||||
self.processor.finalize()
|
self.processor.finalize()
|
||||||
|
|
||||||
|
def _keyboard_interrupt(self):
|
||||||
|
self.exception = exceptions.ChatDataFinished()
|
||||||
|
self.terminate()
|
||||||
|
|
||||||
def _task_finished(self):
|
def _task_finished(self):
|
||||||
'''
|
|
||||||
Listenerを終了する。
|
|
||||||
'''
|
|
||||||
if self.is_alive():
|
if self.is_alive():
|
||||||
self.terminate()
|
self.terminate()
|
||||||
try:
|
try:
|
||||||
@@ -339,7 +363,7 @@ class LiveChatAsync:
|
|||||||
self.exception = e
|
self.exception = e
|
||||||
if not isinstance(e, exceptions.ChatParseException):
|
if not isinstance(e, exceptions.ChatParseException):
|
||||||
self._logger.error(f'Internal exception - {type(e)}{str(e)}')
|
self._logger.error(f'Internal exception - {type(e)}{str(e)}')
|
||||||
self._logger.info(f'[{self._video_id}]終了しました')
|
self._logger.info(f'[{self._video_id}] finished.')
|
||||||
|
|
||||||
def raise_for_status(self):
|
def raise_for_status(self):
|
||||||
if self.exception is not None:
|
if self.exception is not None:
|
||||||
@@ -349,15 +373,3 @@ class LiveChatAsync:
|
|||||||
def _set_exception_handler(cls, handler):
|
def _set_exception_handler(cls, handler):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.set_exception_handler(handler)
|
loop.set_exception_handler(handler)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def shutdown(cls, event, sig=None, handler=None):
|
|
||||||
cls._logger.debug("shutdown...")
|
|
||||||
tasks = [t for t in asyncio.all_tasks() if t is not
|
|
||||||
asyncio.current_task()]
|
|
||||||
[task.cancel() for task in tasks]
|
|
||||||
|
|
||||||
cls._logger.debug("complete remaining tasks...")
|
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.stop()
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import queue
|
|||||||
|
|
||||||
class Buffer(queue.Queue):
|
class Buffer(queue.Queue):
|
||||||
'''
|
'''
|
||||||
チャットデータを格納するバッファの役割を持つFIFOキュー
|
Buffer for storing chat data.
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
max_size : int
|
maxsize : int
|
||||||
格納するチャットブロックの最大個数。0の場合は無限。
|
Maximum number of chat blocks to be stored.
|
||||||
最大値を超える場合は古いチャットブロックから破棄される。
|
If it exceeds the maximum, the oldest chat block will be discarded.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, maxsize=0):
|
def __init__(self, maxsize=0):
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import json
|
|||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
|
||||||
from concurrent.futures import CancelledError, ThreadPoolExecutor
|
from concurrent.futures import CancelledError, ThreadPoolExecutor
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from threading import Event
|
from threading import Event
|
||||||
@@ -11,64 +10,67 @@ from .buffer import Buffer
|
|||||||
from ..parser.live import Parser
|
from ..parser.live import Parser
|
||||||
from .. import config
|
from .. import config
|
||||||
from .. import exceptions
|
from .. import exceptions
|
||||||
|
from .. import util
|
||||||
from ..paramgen import liveparam, arcparam
|
from ..paramgen import liveparam, arcparam
|
||||||
from ..processors.default.processor import DefaultProcessor
|
from ..processors.default.processor import DefaultProcessor
|
||||||
from ..processors.combinator import Combinator
|
from ..processors.combinator import Combinator
|
||||||
from ..util.extract_video_id import extract_video_id
|
|
||||||
|
|
||||||
headers = config.headers
|
headers = config.headers
|
||||||
MAX_RETRY = 10
|
MAX_RETRY = 10
|
||||||
|
|
||||||
|
|
||||||
class LiveChat:
|
class LiveChat:
|
||||||
''' スレッドプールを利用してYouTubeのライブ配信のチャットデータを取得する
|
'''
|
||||||
|
LiveChat object fetches chat data and stores them
|
||||||
|
in a buffer with ThreadpoolExecutor.
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
video_id : str
|
video_id : str
|
||||||
動画ID
|
|
||||||
|
|
||||||
seektime : int
|
seektime : int
|
||||||
(ライブチャット取得時は無視)
|
start position of fetching chat (seconds).
|
||||||
取得開始するアーカイブ済みチャットの経過時間(秒)
|
This option is valid for archived chat only.
|
||||||
マイナス値を指定した場合は、配信開始前のチャットも取得する。
|
If negative value, chat data posted before the start of the broadcast
|
||||||
|
will be retrieved as well.
|
||||||
|
|
||||||
processor : ChatProcessor
|
processor : ChatProcessor
|
||||||
チャットデータを加工するオブジェクト
|
|
||||||
|
|
||||||
buffer : Buffer(maxsize:20[default])
|
buffer : Buffer
|
||||||
チャットデータchat_componentを格納するバッファ。
|
buffer of chat data fetched background.
|
||||||
maxsize : 格納できるchat_componentの個数
|
|
||||||
default値20個。1個で約5~10秒分。
|
|
||||||
|
|
||||||
interruptable : bool
|
interruptable : bool
|
||||||
Ctrl+Cによる処理中断を行うかどうか。
|
Allows keyboard interrupts.
|
||||||
|
Set this parameter to False if your own threading program causes
|
||||||
|
the problem.
|
||||||
|
|
||||||
callback : func
|
callback : func
|
||||||
_listen()関数から一定間隔で自動的に呼びだす関数。
|
function called periodically from _listen().
|
||||||
|
|
||||||
done_callback : func
|
done_callback : func
|
||||||
listener終了時に呼び出すコールバック。
|
function called when listener ends.
|
||||||
|
|
||||||
direct_mode : bool
|
direct_mode : bool
|
||||||
Trueの場合、bufferを使わずにcallbackを呼ぶ。
|
If True, invoke specified callback function without using buffer.
|
||||||
Trueの場合、callbackの設定が必須
|
callback is required. If not, IllegalFunctionCall will be raised.
|
||||||
(設定していない場合IllegalFunctionCall例外を発生させる)
|
|
||||||
|
|
||||||
force_replay : bool
|
force_replay : bool
|
||||||
Trueの場合、ライブチャットが取得できる場合であっても
|
force to fetch archived chat data, even if specified video is live.
|
||||||
強制的にアーカイブ済みチャットを取得する。
|
|
||||||
|
|
||||||
topchat_only : bool
|
topchat_only : bool
|
||||||
Trueの場合、上位チャットのみ取得する。
|
If True, get only top chat.
|
||||||
|
|
||||||
|
replay_continuation : str
|
||||||
|
If this parameter is not None, the processor will attempt to get chat data from continuation.
|
||||||
|
This parameter is only allowed in archived mode.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
---------
|
---------
|
||||||
_executor : ThreadPoolExecutor
|
_executor : ThreadPoolExecutor
|
||||||
チャットデータ取得ループ(_listen)用のスレッド
|
This is used for _listen() loop.
|
||||||
|
|
||||||
_is_alive : bool
|
_is_alive : bool
|
||||||
チャット取得を停止するためのフラグ
|
Flag to stop getting chat.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_setup_finished = False
|
_setup_finished = False
|
||||||
@@ -76,6 +78,7 @@ class LiveChat:
|
|||||||
def __init__(self, video_id,
|
def __init__(self, video_id,
|
||||||
seektime=-1,
|
seektime=-1,
|
||||||
processor=DefaultProcessor(),
|
processor=DefaultProcessor(),
|
||||||
|
client = httpx.Client(http2=True),
|
||||||
buffer=None,
|
buffer=None,
|
||||||
interruptable=True,
|
interruptable=True,
|
||||||
callback=None,
|
callback=None,
|
||||||
@@ -83,9 +86,11 @@ class LiveChat:
|
|||||||
direct_mode=False,
|
direct_mode=False,
|
||||||
force_replay=False,
|
force_replay=False,
|
||||||
topchat_only=False,
|
topchat_only=False,
|
||||||
logger=config.logger(__name__)
|
logger=config.logger(__name__),
|
||||||
|
replay_continuation=None
|
||||||
):
|
):
|
||||||
self._video_id = extract_video_id(video_id)
|
self._client = client
|
||||||
|
self._video_id = util.extract_video_id(video_id)
|
||||||
self.seektime = seektime
|
self.seektime = seektime
|
||||||
if isinstance(processor, tuple):
|
if isinstance(processor, tuple):
|
||||||
self.processor = Combinator(processor)
|
self.processor = Combinator(processor)
|
||||||
@@ -97,39 +102,43 @@ class LiveChat:
|
|||||||
self._executor = ThreadPoolExecutor(max_workers=2)
|
self._executor = ThreadPoolExecutor(max_workers=2)
|
||||||
self._direct_mode = direct_mode
|
self._direct_mode = direct_mode
|
||||||
self._is_alive = True
|
self._is_alive = True
|
||||||
self._is_replay = force_replay
|
self._is_replay = force_replay or (replay_continuation is not None)
|
||||||
self._parser = Parser(is_replay=self._is_replay)
|
self._parser = Parser(is_replay=self._is_replay)
|
||||||
self._pauser = Queue()
|
self._pauser = Queue()
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
self._first_fetch = True
|
self._first_fetch = replay_continuation is None
|
||||||
self._fetch_url = "live_chat/get_live_chat?continuation="
|
self._fetch_url = config._sml if replay_continuation is None else config._smr
|
||||||
self._topchat_only = topchat_only
|
self._topchat_only = topchat_only
|
||||||
self._event = Event()
|
self._dat = ''
|
||||||
|
self._last_offset_ms = 0
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
|
self._event = Event()
|
||||||
|
self.continuation = replay_continuation
|
||||||
|
|
||||||
self.exception = None
|
self.exception = None
|
||||||
if interruptable:
|
if interruptable:
|
||||||
signal.signal(signal.SIGINT, lambda a, b: self.terminate())
|
signal.signal(signal.SIGINT, lambda a, b: self.terminate())
|
||||||
self._setup()
|
self._setup()
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
# direct modeがTrueでcallback未設定の場合例外発生。
|
# An exception is raised when direct mode is true and no callback is set.
|
||||||
if self._direct_mode:
|
if self._direct_mode:
|
||||||
if self._callback is None:
|
if self._callback is None:
|
||||||
raise exceptions.IllegalFunctionCall(
|
raise exceptions.IllegalFunctionCall(
|
||||||
"When direct_mode=True, callback parameter is required.")
|
"When direct_mode=True, callback parameter is required.")
|
||||||
else:
|
else:
|
||||||
# direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
|
# Create a default buffer if `direct_mode` is False and buffer is not set.
|
||||||
if self._buffer is None:
|
if self._buffer is None:
|
||||||
self._buffer = Buffer(maxsize=20)
|
self._buffer = Buffer(maxsize=20)
|
||||||
# callbackが指定されている場合はcallbackを呼ぶループタスクを作成
|
# Create a loop task to call callback if the `callback` param is specified.
|
||||||
if self._callback is None:
|
if self._callback is None:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# callbackを呼ぶループタスクの開始
|
# Start a loop task calling callback function.
|
||||||
self._executor.submit(self._callback_loop, self._callback)
|
self._executor.submit(self._callback_loop, self._callback)
|
||||||
# _listenループタスクの開始
|
# Start a loop task for _listen()
|
||||||
self.listen_task = self._executor.submit(self._startlisten)
|
self.listen_task = self._executor.submit(self._startlisten)
|
||||||
# add_done_callbackの登録
|
# Register add_done_callback
|
||||||
if self._done_callback is None:
|
if self._done_callback is None:
|
||||||
self.listen_task.add_done_callback(self._finish)
|
self.listen_task.add_done_callback(self._finish)
|
||||||
else:
|
else:
|
||||||
@@ -140,8 +149,12 @@ class LiveChat:
|
|||||||
"""Fetch first continuation parameter,
|
"""Fetch first continuation parameter,
|
||||||
create and start _listen loop.
|
create and start _listen loop.
|
||||||
"""
|
"""
|
||||||
initial_continuation = liveparam.getparam(self._video_id, 3)
|
if not self.continuation:
|
||||||
self._listen(initial_continuation)
|
self.continuation = liveparam.getparam(
|
||||||
|
self._video_id,
|
||||||
|
channel_id=util.get_channelid(self._client, self._video_id),
|
||||||
|
past_sec=3)
|
||||||
|
self._listen(self.continuation)
|
||||||
|
|
||||||
def _listen(self, continuation):
|
def _listen(self, continuation):
|
||||||
''' Fetch chat data and store them into buffer,
|
''' Fetch chat data and store them into buffer,
|
||||||
@@ -153,11 +166,14 @@ class LiveChat:
|
|||||||
parameter for next chat data
|
parameter for next chat data
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
with httpx.Client(http2=True) as client:
|
with self._client as client:
|
||||||
while(continuation and self._is_alive):
|
while(continuation and self._is_alive):
|
||||||
continuation = self._check_pause(continuation)
|
continuation = self._check_pause(continuation)
|
||||||
contents = self._get_contents(continuation, client, headers)
|
contents = self._get_contents(continuation, client, headers)
|
||||||
metadata, chatdata = self._parser.parse(contents)
|
metadata, chatdata = self._parser.parse(contents)
|
||||||
|
continuation = metadata.get('continuation')
|
||||||
|
if continuation:
|
||||||
|
self.continuation = continuation
|
||||||
timeout = metadata['timeoutMs'] / 1000
|
timeout = metadata['timeoutMs'] / 1000
|
||||||
chat_component = {
|
chat_component = {
|
||||||
"video_id": self._video_id,
|
"video_id": self._video_id,
|
||||||
@@ -176,16 +192,15 @@ class LiveChat:
|
|||||||
self._buffer.put(chat_component)
|
self._buffer.put(chat_component)
|
||||||
diff_time = timeout - (time.time() - time_mark)
|
diff_time = timeout - (time.time() - time_mark)
|
||||||
self._event.wait(diff_time if diff_time > 0 else 0)
|
self._event.wait(diff_time if diff_time > 0 else 0)
|
||||||
continuation = metadata.get('continuation')
|
self._last_offset_ms = metadata.get('last_offset_ms', 0)
|
||||||
except exceptions.ChatParseException as e:
|
except exceptions.ChatParseException as e:
|
||||||
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
||||||
raise
|
raise
|
||||||
except (TypeError, json.JSONDecodeError):
|
except Exception:
|
||||||
self._logger.error(f"{traceback.format_exc(limit=-1)}")
|
self._logger.error(f"{traceback.format_exc(limit=-1)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
self._logger.debug(f"[{self._video_id}]finished fetching chat.")
|
self._logger.debug(f"[{self._video_id}] finished fetching chat.")
|
||||||
raise exceptions.ChatDataFinished
|
|
||||||
|
|
||||||
def _check_pause(self, continuation):
|
def _check_pause(self, continuation):
|
||||||
if self._pauser.empty():
|
if self._pauser.empty():
|
||||||
@@ -196,7 +211,10 @@ class LiveChat:
|
|||||||
'''
|
'''
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
if not self._is_replay:
|
if not self._is_replay:
|
||||||
continuation = liveparam.getparam(self._video_id, 3)
|
continuation = liveparam.getparam(
|
||||||
|
self._video_id, channel_id=util.get_channelid(httpx.Client(http2=True), self._video_id),
|
||||||
|
past_sec=3, topchat_only=self._topchat_only)
|
||||||
|
|
||||||
return continuation
|
return continuation
|
||||||
|
|
||||||
def _get_contents(self, continuation, client, headers):
|
def _get_contents(self, continuation, client, headers):
|
||||||
@@ -208,58 +226,61 @@ class LiveChat:
|
|||||||
-------
|
-------
|
||||||
'continuationContents' which includes metadata & chat data.
|
'continuationContents' which includes metadata & chat data.
|
||||||
'''
|
'''
|
||||||
livechat_json = (
|
livechat_json = self._get_livechat_json(
|
||||||
self._get_livechat_json(continuation, client, headers)
|
continuation, client, replay=self._is_replay, offset_ms=self._last_offset_ms)
|
||||||
)
|
contents, dat = self._parser.get_contents(livechat_json)
|
||||||
contents = self._parser.get_contents(livechat_json)
|
if self._dat == '' and dat:
|
||||||
|
self._dat = dat
|
||||||
if self._first_fetch:
|
if self._first_fetch:
|
||||||
if contents is None or self._is_replay:
|
if contents is None or self._is_replay:
|
||||||
'''Try to fetch archive chat data.'''
|
'''Try to fetch archive chat data.'''
|
||||||
self._parser.is_replay = True
|
self._parser.is_replay = True
|
||||||
self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation="
|
self._fetch_url = config._smr
|
||||||
continuation = arcparam.getparam(
|
continuation = arcparam.getparam(
|
||||||
self._video_id, self.seektime, self._topchat_only)
|
self._video_id, self.seektime, self._topchat_only, util.get_channelid(client, self._video_id))
|
||||||
livechat_json = (self._get_livechat_json(continuation, client, headers))
|
livechat_json = self._get_livechat_json(
|
||||||
|
continuation, client, replay=True, offset_ms=self.seektime * 1000)
|
||||||
reload_continuation = self._parser.reload_continuation(
|
reload_continuation = self._parser.reload_continuation(
|
||||||
self._parser.get_contents(livechat_json))
|
self._parser.get_contents(livechat_json)[0])
|
||||||
if reload_continuation:
|
if reload_continuation:
|
||||||
livechat_json = (self._get_livechat_json(
|
livechat_json = (self._get_livechat_json(
|
||||||
reload_continuation, client, headers))
|
reload_continuation, client, headers))
|
||||||
contents = self._parser.get_contents(livechat_json)
|
contents, _ = self._parser.get_contents(livechat_json)
|
||||||
self._is_replay = True
|
self._is_replay = True
|
||||||
self._first_fetch = False
|
self._first_fetch = False
|
||||||
return contents
|
return contents
|
||||||
|
|
||||||
def _get_livechat_json(self, continuation, client, headers):
|
def _get_livechat_json(self, continuation, client, replay: bool, offset_ms: int = 0):
|
||||||
'''
|
'''
|
||||||
Get json which includes chat data.
|
Get json which includes chat data.
|
||||||
'''
|
'''
|
||||||
continuation = urllib.parse.quote(continuation)
|
|
||||||
livechat_json = None
|
livechat_json = None
|
||||||
status_code = 0
|
if offset_ms < 0:
|
||||||
url = f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1"
|
offset_ms = 0
|
||||||
|
param = util.get_param(continuation, dat=self._dat, replay=replay, offsetms=offset_ms)
|
||||||
for _ in range(MAX_RETRY + 1):
|
for _ in range(MAX_RETRY + 1):
|
||||||
with client:
|
try:
|
||||||
try:
|
response = client.post(self._fetch_url, json=param)
|
||||||
livechat_json = client.get(url, headers=headers).json()
|
livechat_json = response.json()
|
||||||
break
|
break
|
||||||
except json.JSONDecodeError:
|
except (json.JSONDecodeError, httpx.HTTPError):
|
||||||
time.sleep(1)
|
time.sleep(2)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self._logger.error(f"[{self._video_id}]"
|
self._logger.error(f"[{self._video_id}]"
|
||||||
f"Exceeded retry count. status_code={status_code}")
|
f"Exceeded retry count.")
|
||||||
raise exceptions.RetryExceedMaxCount()
|
raise exceptions.RetryExceedMaxCount()
|
||||||
return livechat_json
|
return livechat_json
|
||||||
|
|
||||||
def _callback_loop(self, callback):
|
def _callback_loop(self, callback):
|
||||||
""" コンストラクタでcallbackを指定している場合、バックグラウンドで
|
""" If a callback is specified in the constructor,
|
||||||
callbackに指定された関数に一定間隔でチャットデータを投げる。
|
it throws chat data at regular intervals to the
|
||||||
|
function specified in the callback in the backgroun
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
callback : func
|
callback : func
|
||||||
加工済みのチャットデータを渡す先の関数。
|
function to which the processed chat data is passed.
|
||||||
"""
|
"""
|
||||||
while self.is_alive():
|
while self.is_alive():
|
||||||
items = self._buffer.get()
|
items = self._buffer.get()
|
||||||
@@ -270,11 +291,13 @@ class LiveChat:
|
|||||||
self._callback(processed_chat)
|
self._callback(processed_chat)
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
""" bufferからデータを取り出し、processorに投げ、
|
"""
|
||||||
加工済みのチャットデータを返す。
|
Retrieves data from the buffer,
|
||||||
|
throws it to the processor,
|
||||||
|
and returns the processed chat data.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
: Processorによって加工されたチャットデータ
|
: Chat data processed by the Processor
|
||||||
"""
|
"""
|
||||||
if self._callback is None:
|
if self._callback is None:
|
||||||
if self.is_alive():
|
if self.is_alive():
|
||||||
@@ -283,7 +306,7 @@ class LiveChat:
|
|||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
raise exceptions.IllegalFunctionCall(
|
raise exceptions.IllegalFunctionCall(
|
||||||
"既にcallbackを登録済みのため、get()は実行できません。")
|
"Callback parameter is already set, so get() cannot be performed.")
|
||||||
|
|
||||||
def is_replay(self):
|
def is_replay(self):
|
||||||
return self._is_replay
|
return self._is_replay
|
||||||
@@ -304,13 +327,15 @@ class LiveChat:
|
|||||||
return self._is_alive
|
return self._is_alive
|
||||||
|
|
||||||
def _finish(self, sender):
|
def _finish(self, sender):
|
||||||
'''Listener終了時のコールバック'''
|
'''Called when the _listen() task finished.'''
|
||||||
try:
|
try:
|
||||||
self._task_finished()
|
self._task_finished()
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
self._logger.debug(f'[{self._video_id}]cancelled:{sender}')
|
self._logger.debug(f'[{self._video_id}] cancelled:{sender}')
|
||||||
|
|
||||||
def terminate(self):
|
def terminate(self):
|
||||||
|
if not self.is_alive():
|
||||||
|
return
|
||||||
if self._pauser.empty():
|
if self._pauser.empty():
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
self._is_alive = False
|
self._is_alive = False
|
||||||
@@ -319,9 +344,6 @@ class LiveChat:
|
|||||||
self.processor.finalize()
|
self.processor.finalize()
|
||||||
|
|
||||||
def _task_finished(self):
|
def _task_finished(self):
|
||||||
'''
|
|
||||||
Listenerを終了する。
|
|
||||||
'''
|
|
||||||
if self.is_alive():
|
if self.is_alive():
|
||||||
self.terminate()
|
self.terminate()
|
||||||
try:
|
try:
|
||||||
@@ -330,7 +352,7 @@ class LiveChat:
|
|||||||
self.exception = e
|
self.exception = e
|
||||||
if not isinstance(e, exceptions.ChatParseException):
|
if not isinstance(e, exceptions.ChatParseException):
|
||||||
self._logger.error(f'Internal exception - {type(e)}{str(e)}')
|
self._logger.error(f'Internal exception - {type(e)}{str(e)}')
|
||||||
self._logger.info(f'[{self._video_id}]終了しました')
|
self._logger.info(f'[{self._video_id}] finished.')
|
||||||
|
|
||||||
def raise_for_status(self):
|
def raise_for_status(self):
|
||||||
if self.exception is not None:
|
if self.exception is not None:
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ class InvalidVideoIdException(Exception):
|
|||||||
'''
|
'''
|
||||||
Thrown when the video_id is not exist (VideoInfo).
|
Thrown when the video_id is not exist (VideoInfo).
|
||||||
'''
|
'''
|
||||||
pass
|
def __init__(self, doc):
|
||||||
|
self.msg = "InvalidVideoIdException"
|
||||||
|
self.doc = doc
|
||||||
|
|
||||||
|
|
||||||
class UnknownConnectionError(Exception):
|
class UnknownConnectionError(Exception):
|
||||||
@@ -47,7 +49,7 @@ class UnknownConnectionError(Exception):
|
|||||||
|
|
||||||
class RetryExceedMaxCount(Exception):
|
class RetryExceedMaxCount(Exception):
|
||||||
'''
|
'''
|
||||||
thrown when the number of retries exceeds the maximum value.
|
Thrown when the number of retries exceeds the maximum value.
|
||||||
'''
|
'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -66,14 +68,14 @@ class FailedExtractContinuation(ChatDataFinished):
|
|||||||
|
|
||||||
class VideoInfoParseError(Exception):
|
class VideoInfoParseError(Exception):
|
||||||
'''
|
'''
|
||||||
thrown when failed to parse video info
|
Base exception when parsing video info.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
class PatternUnmatchError(VideoInfoParseError):
|
class PatternUnmatchError(VideoInfoParseError):
|
||||||
'''
|
'''
|
||||||
thrown when failed to parse video info with unmatched pattern
|
Thrown when failed to parse video info with unmatched pattern.
|
||||||
'''
|
'''
|
||||||
def __init__(self, doc):
|
def __init__(self, doc=''):
|
||||||
self.msg = "PatternUnmatchError"
|
self.msg = "PatternUnmatchError"
|
||||||
self.doc = doc
|
self.doc = doc
|
||||||
|
|||||||
@@ -1,55 +1,37 @@
|
|||||||
from .pb.header_pb2 import Header
|
from . import enc
|
||||||
from .pb.replay_pb2 import Continuation
|
from base64 import urlsafe_b64encode as b64enc
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
import base64
|
|
||||||
|
|
||||||
'''
|
|
||||||
Generate continuation parameter of youtube replay chat.
|
|
||||||
|
|
||||||
Author: taizan-hokuto
|
|
||||||
|
|
||||||
ver 0.0.1 2019.10.05 : Initial release.
|
|
||||||
ver 0.0.2 2020.05.30 : Use Protocol Buffers.
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def _gen_vid(video_id) -> str:
|
def _header(video_id, channel_id) -> str:
|
||||||
header = Header()
|
S1_3 = enc.rs(1, video_id)
|
||||||
header.info.video.id = video_id
|
S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id)
|
||||||
header.terminator = 1
|
S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5)
|
||||||
return base64.urlsafe_b64encode(header.SerializeToString()).decode()
|
S3 = enc.rs(48687757, enc.rs(1, video_id))
|
||||||
|
header_replay = enc.rs(1, S1) + enc.rs(3, S3) + enc.nm(4, 1)
|
||||||
|
return b64enc(header_replay)
|
||||||
|
|
||||||
|
|
||||||
def _build(video_id, seektime, topchat_only) -> str:
|
def _build(video_id, seektime, topchat_only, channel_id) -> str:
|
||||||
chattype = 1
|
chattype = 4 if topchat_only else 1
|
||||||
timestamp = 0
|
|
||||||
if topchat_only:
|
|
||||||
chattype = 4
|
|
||||||
|
|
||||||
fetch_before_start = 3
|
|
||||||
if seektime < 0:
|
if seektime < 0:
|
||||||
fetch_before_start = 4
|
seektime = 0
|
||||||
elif seektime == 0:
|
timestamp = int(seektime * 1000000)
|
||||||
timestamp = 1
|
header = enc.rs(3, _header(video_id, channel_id))
|
||||||
else:
|
timestamp = enc.nm(5, timestamp)
|
||||||
timestamp = int(seektime * 1000000)
|
s6 = enc.nm(6, 0)
|
||||||
continuation = Continuation()
|
s7 = enc.nm(7, 0)
|
||||||
entity = continuation.entity
|
s8 = enc.nm(8, 0)
|
||||||
entity.header = _gen_vid(video_id)
|
s9 = enc.nm(9, 4)
|
||||||
entity.timestamp = timestamp
|
s10 = enc.rs(10, enc.nm(4, 0))
|
||||||
entity.s6 = 0
|
chattype = enc.rs(14, enc.nm(1, 4))
|
||||||
entity.s7 = 0
|
s15 = enc.nm(15, 0)
|
||||||
entity.s8 = 0
|
entity = b''.join((header, timestamp, s6, s7, s8, s9, s10, chattype, s15))
|
||||||
entity.s9 = fetch_before_start
|
continuation = enc.rs(156074452, entity)
|
||||||
entity.s10 = ''
|
return quote(b64enc(continuation).decode())
|
||||||
entity.s12 = chattype
|
|
||||||
entity.chattype.value = chattype
|
|
||||||
entity.s15 = 0
|
|
||||||
return quote(
|
|
||||||
base64.urlsafe_b64encode(continuation.SerializeToString()).decode())
|
|
||||||
|
|
||||||
|
|
||||||
def getparam(video_id, seektime=-1, topchat_only=False) -> str:
|
def getparam(video_id, seektime=0, topchat_only=False, channel_id='') -> str:
|
||||||
'''
|
'''
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
@@ -59,4 +41,4 @@ def getparam(video_id, seektime=-1, topchat_only=False) -> str:
|
|||||||
topchat_only : bool
|
topchat_only : bool
|
||||||
if True, fetch only 'top chat'
|
if True, fetch only 'top chat'
|
||||||
'''
|
'''
|
||||||
return _build(video_id, seektime, topchat_only)
|
return _build(video_id, seektime, topchat_only, channel_id)
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
from base64 import urlsafe_b64encode as b64enc
|
|
||||||
from functools import reduce
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
'''
|
|
||||||
Generate continuation parameter of youtube replay chat.
|
|
||||||
|
|
||||||
Author: taizan-hokuto (2019) @taizan205
|
|
||||||
|
|
||||||
ver 0.0.1 2019.10.05
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def _gen_vid_long(video_id):
|
|
||||||
"""generate video_id parameter.
|
|
||||||
Parameter
|
|
||||||
---------
|
|
||||||
video_id : str
|
|
||||||
|
|
||||||
Return
|
|
||||||
---------
|
|
||||||
byte[] : base64 encoded video_id parameter.
|
|
||||||
"""
|
|
||||||
header_magic = b'\x0A\x0F\x1A\x0D\x0A'
|
|
||||||
header_id = video_id.encode()
|
|
||||||
header_sep_1 = b'\x1A\x13\xEA\xA8\xDD\xB9\x01\x0D\x0A\x0B'
|
|
||||||
header_terminator = b'\x20\x01'
|
|
||||||
|
|
||||||
item = [
|
|
||||||
header_magic,
|
|
||||||
_nval(len(header_id)),
|
|
||||||
header_id,
|
|
||||||
header_sep_1,
|
|
||||||
header_id,
|
|
||||||
header_terminator
|
|
||||||
]
|
|
||||||
|
|
||||||
return urllib.parse.quote(
|
|
||||||
b64enc(reduce(lambda x, y: x + y, item)).decode()
|
|
||||||
).encode()
|
|
||||||
|
|
||||||
|
|
||||||
def _gen_vid(video_id):
|
|
||||||
"""generate video_id parameter.
|
|
||||||
Parameter
|
|
||||||
---------
|
|
||||||
video_id : str
|
|
||||||
|
|
||||||
Return
|
|
||||||
---------
|
|
||||||
bytes : base64 encoded video_id parameter.
|
|
||||||
"""
|
|
||||||
header_magic = b'\x0A\x0F\x1A\x0D\x0A'
|
|
||||||
header_id = video_id.encode()
|
|
||||||
header_terminator = b'\x20\x01'
|
|
||||||
|
|
||||||
item = [
|
|
||||||
header_magic,
|
|
||||||
_nval(len(header_id)),
|
|
||||||
header_id,
|
|
||||||
header_terminator
|
|
||||||
]
|
|
||||||
|
|
||||||
return urllib.parse.quote(
|
|
||||||
b64enc(reduce(lambda x, y: x + y, item)).decode()
|
|
||||||
).encode()
|
|
||||||
|
|
||||||
|
|
||||||
def _nval(val):
|
|
||||||
"""convert value to byte array"""
|
|
||||||
if val < 0:
|
|
||||||
raise ValueError
|
|
||||||
buf = b''
|
|
||||||
while val >> 7:
|
|
||||||
m = val & 0xFF | 0x80
|
|
||||||
buf += m.to_bytes(1, 'big')
|
|
||||||
val >>= 7
|
|
||||||
buf += val.to_bytes(1, 'big')
|
|
||||||
return buf
|
|
||||||
|
|
||||||
|
|
||||||
def _build(video_id, seektime, topchat_only):
|
|
||||||
switch_01 = b'\x04' if topchat_only else b'\x01'
|
|
||||||
if seektime < 0:
|
|
||||||
raise ValueError("seektime must be greater than or equal to zero.")
|
|
||||||
if seektime == 0:
|
|
||||||
times = b''
|
|
||||||
else:
|
|
||||||
times = _nval(int(seektime * 1000))
|
|
||||||
if seektime > 0:
|
|
||||||
_len_time = b'\x5A' + (len(times) + 1).to_bytes(1, 'big') + b'\x10'
|
|
||||||
else:
|
|
||||||
_len_time = b''
|
|
||||||
|
|
||||||
header_magic = b'\xA2\x9D\xB0\xD3\x04'
|
|
||||||
sep_0 = b'\x1A'
|
|
||||||
vid = _gen_vid(video_id)
|
|
||||||
_tag = b'\x40\x01'
|
|
||||||
timestamp1 = times
|
|
||||||
sep_1 = b'\x60\x04\x72\x02\x08'
|
|
||||||
terminator = b'\x78\x01'
|
|
||||||
|
|
||||||
body = [
|
|
||||||
sep_0,
|
|
||||||
_nval(len(vid)),
|
|
||||||
vid,
|
|
||||||
_tag,
|
|
||||||
_len_time,
|
|
||||||
timestamp1,
|
|
||||||
sep_1,
|
|
||||||
switch_01,
|
|
||||||
terminator
|
|
||||||
]
|
|
||||||
|
|
||||||
body = reduce(lambda x, y: x + y, body)
|
|
||||||
|
|
||||||
return urllib.parse.quote(
|
|
||||||
b64enc(header_magic + _nval(len(body)) + body
|
|
||||||
).decode()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def getparam(video_id, seektime=0.0, topchat_only=False):
|
|
||||||
'''
|
|
||||||
Parameter
|
|
||||||
---------
|
|
||||||
seektime : int
|
|
||||||
unit:seconds
|
|
||||||
start position of fetching chat data.
|
|
||||||
topchat_only : bool
|
|
||||||
if True, fetch only 'top chat'
|
|
||||||
'''
|
|
||||||
return _build(video_id, seektime, topchat_only)
|
|
||||||
24
pytchat/paramgen/enc.py
Normal file
24
pytchat/paramgen/enc.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
def vn(val):
|
||||||
|
if val < 0:
|
||||||
|
raise ValueError
|
||||||
|
buf = b''
|
||||||
|
while val >> 7:
|
||||||
|
m = val & 0xFF | 0x80
|
||||||
|
buf += m.to_bytes(1, 'big')
|
||||||
|
val >>= 7
|
||||||
|
buf += val.to_bytes(1, 'big')
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
|
def tp(a, b, ary):
|
||||||
|
return vn((b << 3) | a) + ary
|
||||||
|
|
||||||
|
|
||||||
|
def rs(a, ary):
|
||||||
|
if isinstance(ary, str):
|
||||||
|
ary = ary.encode()
|
||||||
|
return tp(2, a, vn(len(ary)) + ary)
|
||||||
|
|
||||||
|
|
||||||
|
def nm(a, ary):
|
||||||
|
return tp(0, a, vn(ary))
|
||||||
@@ -1,69 +1,51 @@
|
|||||||
from .pb.header_pb2 import Header
|
|
||||||
from .pb.live_pb2 import Continuation
|
|
||||||
from urllib.parse import quote
|
|
||||||
import base64
|
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
from . import enc
|
||||||
'''
|
from base64 import urlsafe_b64encode as b64enc
|
||||||
Generate continuation parameter of youtube live chat.
|
from urllib.parse import quote
|
||||||
|
|
||||||
Author: taizan-hokuto
|
|
||||||
|
|
||||||
ver 0.0.1 2019.10.05 : Initial release.
|
|
||||||
ver 0.0.2 2020.05.30 : Use Protocol Buffers.
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def _gen_vid(video_id) -> str:
|
def _header(video_id, channel_id) -> str:
|
||||||
"""generate video_id parameter.
|
S1_3 = enc.rs(1, video_id)
|
||||||
Parameter
|
S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id)
|
||||||
---------
|
S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5)
|
||||||
video_id : str
|
S3 = enc.rs(48687757, enc.rs(1, video_id))
|
||||||
|
header_replay = enc.rs(1, S1) + enc.rs(3, S3) + enc.nm(4, 1)
|
||||||
Return
|
return b64enc(header_replay)
|
||||||
---------
|
|
||||||
str : base64 encoded video_id parameter.
|
|
||||||
"""
|
|
||||||
header = Header()
|
|
||||||
header.info.video.id = video_id
|
|
||||||
header.terminator = 1
|
|
||||||
return base64.urlsafe_b64encode(header.SerializeToString()).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def _build(video_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
|
def _build(video_id, channel_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
|
||||||
chattype = 1
|
chattype = 4 if topchat_only else 1
|
||||||
if topchat_only:
|
|
||||||
chattype = 4
|
|
||||||
continuation = Continuation()
|
|
||||||
entity = continuation.entity
|
|
||||||
|
|
||||||
entity.header = _gen_vid(video_id)
|
b1 = enc.nm(1, 0)
|
||||||
entity.timestamp1 = ts1
|
b2 = enc.nm(2, 0)
|
||||||
entity.s6 = 0
|
b3 = enc.nm(3, 0)
|
||||||
entity.s7 = 0
|
b4 = enc.nm(4, 0)
|
||||||
entity.s8 = 1
|
b7 = enc.rs(7, '')
|
||||||
entity.body.b1 = 0
|
b8 = enc.nm(8, 0)
|
||||||
entity.body.b2 = 0
|
b9 = enc.rs(9, '')
|
||||||
entity.body.b3 = 0
|
timestamp2 = enc.nm(10, ts2)
|
||||||
entity.body.b4 = 0
|
b11 = enc.nm(11, 3)
|
||||||
entity.body.b7 = ''
|
b15 = enc.nm(15, 0)
|
||||||
entity.body.b8 = 0
|
|
||||||
entity.body.b9 = ''
|
|
||||||
entity.body.timestamp2 = ts2
|
|
||||||
entity.body.b11 = 3
|
|
||||||
entity.body.b15 = 0
|
|
||||||
entity.timestamp3 = ts3
|
|
||||||
entity.timestamp4 = ts4
|
|
||||||
entity.s13 = chattype
|
|
||||||
entity.chattype.value = chattype
|
|
||||||
entity.s17 = 0
|
|
||||||
entity.str19.value = 0
|
|
||||||
entity.timestamp5 = ts5
|
|
||||||
|
|
||||||
return quote(
|
header = enc.rs(3, _header(video_id, channel_id))
|
||||||
base64.urlsafe_b64encode(continuation.SerializeToString()).decode()
|
timestamp1 = enc.nm(5, ts1)
|
||||||
)
|
s6 = enc.nm(6, 0)
|
||||||
|
s7 = enc.nm(7, 0)
|
||||||
|
s8 = enc.nm(8, 1)
|
||||||
|
body = enc.rs(9, b''.join(
|
||||||
|
(b1, b2, b3, b4, b7, b8, b9, timestamp2, b11, b15)))
|
||||||
|
timestamp3 = enc.nm(10, ts3)
|
||||||
|
timestamp4 = enc.nm(11, ts4)
|
||||||
|
s13 = enc.nm(13, chattype)
|
||||||
|
chattype = enc.rs(16, enc.nm(1, chattype))
|
||||||
|
s17 = enc.nm(17, 0)
|
||||||
|
str19 = enc.rs(19, enc.nm(1, 0))
|
||||||
|
timestamp5 = enc.nm(20, ts5)
|
||||||
|
entity = b''.join((header, timestamp1, s6, s7, s8, body, timestamp3,
|
||||||
|
timestamp4, s13, chattype, s17, str19, timestamp5))
|
||||||
|
continuation = enc.rs(119693434, entity)
|
||||||
|
return quote(b64enc(continuation).decode())
|
||||||
|
|
||||||
|
|
||||||
def _times(past_sec):
|
def _times(past_sec):
|
||||||
@@ -76,7 +58,7 @@ def _times(past_sec):
|
|||||||
return list(map(lambda x: int(x * 1000000), [_ts1, _ts2, _ts3, _ts4, _ts5]))
|
return list(map(lambda x: int(x * 1000000), [_ts1, _ts2, _ts3, _ts4, _ts5]))
|
||||||
|
|
||||||
|
|
||||||
def getparam(video_id, past_sec=0, topchat_only=False) -> str:
|
def getparam(video_id, channel_id, past_sec=0, topchat_only=False) -> str:
|
||||||
'''
|
'''
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
@@ -85,4 +67,4 @@ def getparam(video_id, past_sec=0, topchat_only=False) -> str:
|
|||||||
topchat_only : bool
|
topchat_only : bool
|
||||||
if True, fetch only 'top chat'
|
if True, fetch only 'top chat'
|
||||||
'''
|
'''
|
||||||
return _build(video_id, *_times(past_sec), topchat_only)
|
return _build(video_id, channel_id, *_times(past_sec), topchat_only)
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
||||||
# source: header.proto
|
|
||||||
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from google.protobuf import message as _message
|
|
||||||
from google.protobuf import reflection as _reflection
|
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
|
||||||
# @@protoc_insertion_point(imports)
|
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
|
||||||
name='header.proto',
|
|
||||||
package='',
|
|
||||||
syntax='proto3',
|
|
||||||
serialized_options=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
serialized_pb=b'\n\x0cheader.proto\"\x13\n\x05Video\x12\n\n\x02id\x18\x01 \x01(\t\"#\n\nHeaderInfo\x12\x15\n\x05video\x18\x01 \x01(\x0b\x32\x06.Video\"7\n\x06Header\x12\x19\n\x04info\x18\x01 \x01(\x0b\x32\x0b.HeaderInfo\x12\x12\n\nterminator\x18\x04 \x01(\x05\x62\x06proto3'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_VIDEO = _descriptor.Descriptor(
|
|
||||||
name='Video',
|
|
||||||
full_name='Video',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='id', full_name='Video.id', index=0,
|
|
||||||
number=1, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=16,
|
|
||||||
serialized_end=35,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_HEADERINFO = _descriptor.Descriptor(
|
|
||||||
name='HeaderInfo',
|
|
||||||
full_name='HeaderInfo',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='video', full_name='HeaderInfo.video', index=0,
|
|
||||||
number=1, type=11, cpp_type=10, label=1,
|
|
||||||
has_default_value=False, default_value=None,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=37,
|
|
||||||
serialized_end=72,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_HEADER = _descriptor.Descriptor(
|
|
||||||
name='Header',
|
|
||||||
full_name='Header',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='info', full_name='Header.info', index=0,
|
|
||||||
number=1, type=11, cpp_type=10, label=1,
|
|
||||||
has_default_value=False, default_value=None,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='terminator', full_name='Header.terminator', index=1,
|
|
||||||
number=4, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=74,
|
|
||||||
serialized_end=129,
|
|
||||||
)
|
|
||||||
|
|
||||||
_HEADERINFO.fields_by_name['video'].message_type = _VIDEO
|
|
||||||
_HEADER.fields_by_name['info'].message_type = _HEADERINFO
|
|
||||||
DESCRIPTOR.message_types_by_name['Video'] = _VIDEO
|
|
||||||
DESCRIPTOR.message_types_by_name['HeaderInfo'] = _HEADERINFO
|
|
||||||
DESCRIPTOR.message_types_by_name['Header'] = _HEADER
|
|
||||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
|
||||||
|
|
||||||
Video = _reflection.GeneratedProtocolMessageType('Video', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _VIDEO,
|
|
||||||
'__module__' : 'header_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:Video)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(Video)
|
|
||||||
|
|
||||||
HeaderInfo = _reflection.GeneratedProtocolMessageType('HeaderInfo', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _HEADERINFO,
|
|
||||||
'__module__' : 'header_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:HeaderInfo)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(HeaderInfo)
|
|
||||||
|
|
||||||
Header = _reflection.GeneratedProtocolMessageType('Header', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _HEADER,
|
|
||||||
'__module__' : 'header_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:Header)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(Header)
|
|
||||||
|
|
||||||
|
|
||||||
# @@protoc_insertion_point(module_scope)
|
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
||||||
# source: live.proto
|
|
||||||
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from google.protobuf import message as _message
|
|
||||||
from google.protobuf import reflection as _reflection
|
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
|
||||||
# @@protoc_insertion_point(imports)
|
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
|
||||||
name='live.proto',
|
|
||||||
package='live',
|
|
||||||
syntax='proto3',
|
|
||||||
serialized_options=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
serialized_pb=b'\n\nlive.proto\x12\x04live\"\x88\x01\n\x04\x42ody\x12\n\n\x02\x62\x31\x18\x01 \x01(\x05\x12\n\n\x02\x62\x32\x18\x02 \x01(\x05\x12\n\n\x02\x62\x33\x18\x03 \x01(\x05\x12\n\n\x02\x62\x34\x18\x04 \x01(\x05\x12\n\n\x02\x62\x37\x18\x07 \x01(\t\x12\n\n\x02\x62\x38\x18\x08 \x01(\x05\x12\n\n\x02\x62\x39\x18\t \x01(\t\x12\x12\n\ntimestamp2\x18\n \x01(\x03\x12\x0b\n\x03\x62\x31\x31\x18\x0b \x01(\x05\x12\x0b\n\x03\x62\x31\x35\x18\x0f \x01(\x05\"\x19\n\x08\x43hatType\x12\r\n\x05value\x18\x01 \x01(\x05\"\x16\n\x05STR19\x12\r\n\x05value\x18\x01 \x01(\x05\"\x8a\x02\n\x12\x43ontinuationEntity\x12\x0e\n\x06header\x18\x03 \x01(\t\x12\x12\n\ntimestamp1\x18\x05 \x01(\x03\x12\n\n\x02s6\x18\x06 \x01(\x05\x12\n\n\x02s7\x18\x07 \x01(\x05\x12\n\n\x02s8\x18\x08 \x01(\x05\x12\x18\n\x04\x62ody\x18\t \x01(\x0b\x32\n.live.Body\x12\x12\n\ntimestamp3\x18\n \x01(\x03\x12\x12\n\ntimestamp4\x18\x0b \x01(\x03\x12\x0b\n\x03s13\x18\r \x01(\x05\x12 \n\x08\x63hattype\x18\x10 \x01(\x0b\x32\x0e.live.ChatType\x12\x0b\n\x03s17\x18\x11 \x01(\x05\x12\x1a\n\x05str19\x18\x13 \x01(\x0b\x32\x0b.live.STR19\x12\x12\n\ntimestamp5\x18\x14 \x01(\x03\";\n\x0c\x43ontinuation\x12+\n\x06\x65ntity\x18\xfa\xc0\x89\x39 \x01(\x0b\x32\x18.live.ContinuationEntityb\x06proto3'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_BODY = _descriptor.Descriptor(
|
|
||||||
name='Body',
|
|
||||||
full_name='live.Body',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b1', full_name='live.Body.b1', index=0,
|
|
||||||
number=1, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b2', full_name='live.Body.b2', index=1,
|
|
||||||
number=2, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b3', full_name='live.Body.b3', index=2,
|
|
||||||
number=3, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b4', full_name='live.Body.b4', index=3,
|
|
||||||
number=4, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b7', full_name='live.Body.b7', index=4,
|
|
||||||
number=7, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b8', full_name='live.Body.b8', index=5,
|
|
||||||
number=8, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b9', full_name='live.Body.b9', index=6,
|
|
||||||
number=9, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='timestamp2', full_name='live.Body.timestamp2', index=7,
|
|
||||||
number=10, type=3, cpp_type=2, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b11', full_name='live.Body.b11', index=8,
|
|
||||||
number=11, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='b15', full_name='live.Body.b15', index=9,
|
|
||||||
number=15, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=21,
|
|
||||||
serialized_end=157,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_CHATTYPE = _descriptor.Descriptor(
|
|
||||||
name='ChatType',
|
|
||||||
full_name='live.ChatType',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='value', full_name='live.ChatType.value', index=0,
|
|
||||||
number=1, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=159,
|
|
||||||
serialized_end=184,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_STR19 = _descriptor.Descriptor(
|
|
||||||
name='STR19',
|
|
||||||
full_name='live.STR19',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='value', full_name='live.STR19.value', index=0,
|
|
||||||
number=1, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=186,
|
|
||||||
serialized_end=208,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_CONTINUATIONENTITY = _descriptor.Descriptor(
|
|
||||||
name='ContinuationEntity',
|
|
||||||
full_name='live.ContinuationEntity',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='header', full_name='live.ContinuationEntity.header', index=0,
|
|
||||||
number=3, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='timestamp1', full_name='live.ContinuationEntity.timestamp1', index=1,
|
|
||||||
number=5, type=3, cpp_type=2, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s6', full_name='live.ContinuationEntity.s6', index=2,
|
|
||||||
number=6, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s7', full_name='live.ContinuationEntity.s7', index=3,
|
|
||||||
number=7, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s8', full_name='live.ContinuationEntity.s8', index=4,
|
|
||||||
number=8, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='body', full_name='live.ContinuationEntity.body', index=5,
|
|
||||||
number=9, type=11, cpp_type=10, label=1,
|
|
||||||
has_default_value=False, default_value=None,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='timestamp3', full_name='live.ContinuationEntity.timestamp3', index=6,
|
|
||||||
number=10, type=3, cpp_type=2, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='timestamp4', full_name='live.ContinuationEntity.timestamp4', index=7,
|
|
||||||
number=11, type=3, cpp_type=2, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s13', full_name='live.ContinuationEntity.s13', index=8,
|
|
||||||
number=13, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='chattype', full_name='live.ContinuationEntity.chattype', index=9,
|
|
||||||
number=16, type=11, cpp_type=10, label=1,
|
|
||||||
has_default_value=False, default_value=None,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s17', full_name='live.ContinuationEntity.s17', index=10,
|
|
||||||
number=17, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='str19', full_name='live.ContinuationEntity.str19', index=11,
|
|
||||||
number=19, type=11, cpp_type=10, label=1,
|
|
||||||
has_default_value=False, default_value=None,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='timestamp5', full_name='live.ContinuationEntity.timestamp5', index=12,
|
|
||||||
number=20, type=3, cpp_type=2, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=211,
|
|
||||||
serialized_end=477,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_CONTINUATION = _descriptor.Descriptor(
|
|
||||||
name='Continuation',
|
|
||||||
full_name='live.Continuation',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='entity', full_name='live.Continuation.entity', index=0,
|
|
||||||
number=119693434, type=11, cpp_type=10, label=1,
|
|
||||||
has_default_value=False, default_value=None,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=479,
|
|
||||||
serialized_end=538,
|
|
||||||
)
|
|
||||||
|
|
||||||
_CONTINUATIONENTITY.fields_by_name['body'].message_type = _BODY
|
|
||||||
_CONTINUATIONENTITY.fields_by_name['chattype'].message_type = _CHATTYPE
|
|
||||||
_CONTINUATIONENTITY.fields_by_name['str19'].message_type = _STR19
|
|
||||||
_CONTINUATION.fields_by_name['entity'].message_type = _CONTINUATIONENTITY
|
|
||||||
DESCRIPTOR.message_types_by_name['Body'] = _BODY
|
|
||||||
DESCRIPTOR.message_types_by_name['ChatType'] = _CHATTYPE
|
|
||||||
DESCRIPTOR.message_types_by_name['STR19'] = _STR19
|
|
||||||
DESCRIPTOR.message_types_by_name['ContinuationEntity'] = _CONTINUATIONENTITY
|
|
||||||
DESCRIPTOR.message_types_by_name['Continuation'] = _CONTINUATION
|
|
||||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
|
||||||
|
|
||||||
Body = _reflection.GeneratedProtocolMessageType('Body', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _BODY,
|
|
||||||
'__module__' : 'live_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:live.Body)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(Body)
|
|
||||||
|
|
||||||
ChatType = _reflection.GeneratedProtocolMessageType('ChatType', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _CHATTYPE,
|
|
||||||
'__module__' : 'live_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:live.ChatType)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(ChatType)
|
|
||||||
|
|
||||||
STR19 = _reflection.GeneratedProtocolMessageType('STR19', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _STR19,
|
|
||||||
'__module__' : 'live_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:live.STR19)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(STR19)
|
|
||||||
|
|
||||||
ContinuationEntity = _reflection.GeneratedProtocolMessageType('ContinuationEntity', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _CONTINUATIONENTITY,
|
|
||||||
'__module__' : 'live_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:live.ContinuationEntity)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(ContinuationEntity)
|
|
||||||
|
|
||||||
Continuation = _reflection.GeneratedProtocolMessageType('Continuation', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _CONTINUATION,
|
|
||||||
'__module__' : 'live_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:live.Continuation)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(Continuation)
|
|
||||||
|
|
||||||
|
|
||||||
# @@protoc_insertion_point(module_scope)
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
||||||
# source: replay.proto
|
|
||||||
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
|
||||||
from google.protobuf import message as _message
|
|
||||||
from google.protobuf import reflection as _reflection
|
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
|
||||||
# @@protoc_insertion_point(imports)
|
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
|
||||||
name='replay.proto',
|
|
||||||
package='replay',
|
|
||||||
syntax='proto3',
|
|
||||||
serialized_options=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
serialized_pb=b'\n\x0creplay.proto\x12\x06replay\"\x19\n\x08\x43hatType\x12\r\n\x05value\x18\x01 \x01(\x05\"\xb2\x01\n\x12\x43ontinuationEntity\x12\x0e\n\x06header\x18\x03 \x01(\t\x12\x11\n\ttimestamp\x18\x05 \x01(\x03\x12\n\n\x02s6\x18\x06 \x01(\x05\x12\n\n\x02s7\x18\x07 \x01(\x05\x12\n\n\x02s8\x18\x08 \x01(\x05\x12\n\n\x02s9\x18\t \x01(\x05\x12\x0b\n\x03s10\x18\n \x01(\t\x12\x0b\n\x03s12\x18\x0c \x01(\x05\x12\"\n\x08\x63hattype\x18\x0e \x01(\x0b\x32\x10.replay.ChatType\x12\x0b\n\x03s15\x18\x0f \x01(\x05\"=\n\x0c\x43ontinuation\x12-\n\x06\x65ntity\x18\xd4\x83\xb6J \x01(\x0b\x32\x1a.replay.ContinuationEntityb\x06proto3'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_CHATTYPE = _descriptor.Descriptor(
|
|
||||||
name='ChatType',
|
|
||||||
full_name='replay.ChatType',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='value', full_name='replay.ChatType.value', index=0,
|
|
||||||
number=1, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=24,
|
|
||||||
serialized_end=49,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_CONTINUATIONENTITY = _descriptor.Descriptor(
|
|
||||||
name='ContinuationEntity',
|
|
||||||
full_name='replay.ContinuationEntity',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='header', full_name='replay.ContinuationEntity.header', index=0,
|
|
||||||
number=3, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='timestamp', full_name='replay.ContinuationEntity.timestamp', index=1,
|
|
||||||
number=5, type=3, cpp_type=2, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s6', full_name='replay.ContinuationEntity.s6', index=2,
|
|
||||||
number=6, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s7', full_name='replay.ContinuationEntity.s7', index=3,
|
|
||||||
number=7, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s8', full_name='replay.ContinuationEntity.s8', index=4,
|
|
||||||
number=8, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s9', full_name='replay.ContinuationEntity.s9', index=5,
|
|
||||||
number=9, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s10', full_name='replay.ContinuationEntity.s10', index=6,
|
|
||||||
number=10, type=9, cpp_type=9, label=1,
|
|
||||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s12', full_name='replay.ContinuationEntity.s12', index=7,
|
|
||||||
number=12, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='chattype', full_name='replay.ContinuationEntity.chattype', index=8,
|
|
||||||
number=14, type=11, cpp_type=10, label=1,
|
|
||||||
has_default_value=False, default_value=None,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='s15', full_name='replay.ContinuationEntity.s15', index=9,
|
|
||||||
number=15, type=5, cpp_type=1, label=1,
|
|
||||||
has_default_value=False, default_value=0,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=52,
|
|
||||||
serialized_end=230,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_CONTINUATION = _descriptor.Descriptor(
|
|
||||||
name='Continuation',
|
|
||||||
full_name='replay.Continuation',
|
|
||||||
filename=None,
|
|
||||||
file=DESCRIPTOR,
|
|
||||||
containing_type=None,
|
|
||||||
create_key=_descriptor._internal_create_key,
|
|
||||||
fields=[
|
|
||||||
_descriptor.FieldDescriptor(
|
|
||||||
name='entity', full_name='replay.Continuation.entity', index=0,
|
|
||||||
number=156074452, type=11, cpp_type=10, label=1,
|
|
||||||
has_default_value=False, default_value=None,
|
|
||||||
message_type=None, enum_type=None, containing_type=None,
|
|
||||||
is_extension=False, extension_scope=None,
|
|
||||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
|
||||||
],
|
|
||||||
extensions=[
|
|
||||||
],
|
|
||||||
nested_types=[],
|
|
||||||
enum_types=[
|
|
||||||
],
|
|
||||||
serialized_options=None,
|
|
||||||
is_extendable=False,
|
|
||||||
syntax='proto3',
|
|
||||||
extension_ranges=[],
|
|
||||||
oneofs=[
|
|
||||||
],
|
|
||||||
serialized_start=232,
|
|
||||||
serialized_end=293,
|
|
||||||
)
|
|
||||||
|
|
||||||
_CONTINUATIONENTITY.fields_by_name['chattype'].message_type = _CHATTYPE
|
|
||||||
_CONTINUATION.fields_by_name['entity'].message_type = _CONTINUATIONENTITY
|
|
||||||
DESCRIPTOR.message_types_by_name['ChatType'] = _CHATTYPE
|
|
||||||
DESCRIPTOR.message_types_by_name['ContinuationEntity'] = _CONTINUATIONENTITY
|
|
||||||
DESCRIPTOR.message_types_by_name['Continuation'] = _CONTINUATION
|
|
||||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
|
||||||
|
|
||||||
ChatType = _reflection.GeneratedProtocolMessageType('ChatType', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _CHATTYPE,
|
|
||||||
'__module__' : 'replay_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:replay.ChatType)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(ChatType)
|
|
||||||
|
|
||||||
ContinuationEntity = _reflection.GeneratedProtocolMessageType('ContinuationEntity', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _CONTINUATIONENTITY,
|
|
||||||
'__module__' : 'replay_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:replay.ContinuationEntity)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(ContinuationEntity)
|
|
||||||
|
|
||||||
Continuation = _reflection.GeneratedProtocolMessageType('Continuation', (_message.Message,), {
|
|
||||||
'DESCRIPTOR' : _CONTINUATION,
|
|
||||||
'__module__' : 'replay_pb2'
|
|
||||||
# @@protoc_insertion_point(class_scope:replay.Continuation)
|
|
||||||
})
|
|
||||||
_sym_db.RegisterMessage(Continuation)
|
|
||||||
|
|
||||||
|
|
||||||
# @@protoc_insertion_point(module_scope)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
message Video {
|
|
||||||
string id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message HeaderInfo {
|
|
||||||
Video video = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Header {
|
|
||||||
HeaderInfo info = 1;
|
|
||||||
int32 terminator = 4;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package live;
|
|
||||||
|
|
||||||
message Body {
|
|
||||||
int32 b1 = 1;
|
|
||||||
int32 b2 = 2;
|
|
||||||
int32 b3 = 3;
|
|
||||||
int32 b4 = 4;
|
|
||||||
string b7 = 7;
|
|
||||||
int32 b8 = 8;
|
|
||||||
string b9 = 9;
|
|
||||||
int64 timestamp2 = 10;
|
|
||||||
int32 b11 = 11;
|
|
||||||
int32 b15 = 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ChatType {
|
|
||||||
int32 value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message STR19 {
|
|
||||||
int32 value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ContinuationEntity {
|
|
||||||
string header = 3;
|
|
||||||
int64 timestamp1 = 5;
|
|
||||||
int32 s6 = 6;
|
|
||||||
int32 s7 = 7;
|
|
||||||
int32 s8 = 8;
|
|
||||||
Body body = 9;
|
|
||||||
int64 timestamp3 = 10;
|
|
||||||
int64 timestamp4 = 11;
|
|
||||||
int32 s13 = 13;
|
|
||||||
ChatType chattype = 16;
|
|
||||||
int32 s17 = 17;
|
|
||||||
STR19 str19 = 19;
|
|
||||||
int64 timestamp5 = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Continuation {
|
|
||||||
ContinuationEntity entity = 119693434;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package replay;
|
|
||||||
|
|
||||||
message ChatType {
|
|
||||||
int32 value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ContinuationEntity {
|
|
||||||
string header = 3;
|
|
||||||
int64 timestamp = 5;
|
|
||||||
int32 s6 = 6;
|
|
||||||
int32 s7 = 7;
|
|
||||||
int32 s8 = 8;
|
|
||||||
int32 s9 = 9;
|
|
||||||
string s10 = 10;
|
|
||||||
int32 s12 = 12;
|
|
||||||
ChatType chattype = 14;
|
|
||||||
int32 s15 = 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Continuation {
|
|
||||||
ContinuationEntity entity = 156074452;
|
|
||||||
}
|
|
||||||
@@ -8,20 +8,32 @@ from .. import exceptions
|
|||||||
|
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
|
'''
|
||||||
|
Parser of chat json.
|
||||||
|
|
||||||
|
Parameter
|
||||||
|
----------
|
||||||
|
is_replay : bool
|
||||||
|
|
||||||
__slots__ = ['is_replay']
|
exception_holder : Object [default:Npne]
|
||||||
|
The object holding exceptions.
|
||||||
|
This is passed from the parent livechat object.
|
||||||
|
'''
|
||||||
|
__slots__ = ['is_replay', 'exception_holder']
|
||||||
|
|
||||||
def __init__(self, is_replay):
|
def __init__(self, is_replay, exception_holder=None):
|
||||||
self.is_replay = is_replay
|
self.is_replay = is_replay
|
||||||
|
self.exception_holder = exception_holder
|
||||||
|
|
||||||
def get_contents(self, jsn):
|
def get_contents(self, jsn):
|
||||||
if jsn is None:
|
if jsn is None:
|
||||||
raise exceptions.IllegalFunctionCall('Called with none JSON object.')
|
self.raise_exception(exceptions.IllegalFunctionCall('Called with none JSON object.'))
|
||||||
if jsn['response']['responseContext'].get('errors'):
|
if jsn.get("responseContext", {}).get("errors"):
|
||||||
raise exceptions.ResponseContextError(
|
raise exceptions.ResponseContextError(
|
||||||
'The video_id would be wrong, or video is deleted or private.')
|
'The video_id would be wrong, or video is deleted or private.')
|
||||||
contents = jsn['response'].get('continuationContents')
|
contents = jsn.get('continuationContents')
|
||||||
return contents
|
visitor_data = jsn.get("responseContext", {}).get("visitorData")
|
||||||
|
return contents, visitor_data
|
||||||
|
|
||||||
def parse(self, contents):
|
def parse(self, contents):
|
||||||
"""
|
"""
|
||||||
@@ -42,11 +54,11 @@ class Parser:
|
|||||||
|
|
||||||
if contents is None:
|
if contents is None:
|
||||||
'''Broadcasting end or cannot fetch chat stream'''
|
'''Broadcasting end or cannot fetch chat stream'''
|
||||||
raise exceptions.NoContents('Chat data stream is empty.')
|
self.raise_exception(exceptions.NoContents('Chat data stream is empty.'))
|
||||||
|
|
||||||
cont = contents['liveChatContinuation']['continuations'][0]
|
cont = contents['liveChatContinuation']['continuations'][0]
|
||||||
if cont is None:
|
if cont is None:
|
||||||
raise exceptions.NoContinuation('No Continuation')
|
self.raise_exception(exceptions.NoContinuation('No Continuation'))
|
||||||
metadata = (cont.get('invalidationContinuationData')
|
metadata = (cont.get('invalidationContinuationData')
|
||||||
or cont.get('timedContinuationData')
|
or cont.get('timedContinuationData')
|
||||||
or cont.get('reloadContinuationData')
|
or cont.get('reloadContinuationData')
|
||||||
@@ -54,13 +66,13 @@ class Parser:
|
|||||||
)
|
)
|
||||||
if metadata is None:
|
if metadata is None:
|
||||||
if cont.get("playerSeekContinuationData"):
|
if cont.get("playerSeekContinuationData"):
|
||||||
raise exceptions.ChatDataFinished('Finished chat data')
|
self.raise_exception(exceptions.ChatDataFinished('Finished chat data'))
|
||||||
unknown = list(cont.keys())[0]
|
unknown = list(cont.keys())[0]
|
||||||
if unknown:
|
if unknown:
|
||||||
raise exceptions.ReceivedUnknownContinuation(
|
self.raise_exception(exceptions.ReceivedUnknownContinuation(
|
||||||
f"Received unknown continuation type:{unknown}")
|
f"Received unknown continuation type:{unknown}"))
|
||||||
else:
|
else:
|
||||||
raise exceptions.FailedExtractContinuation('Cannot extract continuation data')
|
self.raise_exception(exceptions.FailedExtractContinuation('Cannot extract continuation data'))
|
||||||
return self._create_data(metadata, contents)
|
return self._create_data(metadata, contents)
|
||||||
|
|
||||||
def reload_continuation(self, contents):
|
def reload_continuation(self, contents):
|
||||||
@@ -72,8 +84,9 @@ class Parser:
|
|||||||
"""
|
"""
|
||||||
if contents is None:
|
if contents is None:
|
||||||
'''Broadcasting end or cannot fetch chat stream'''
|
'''Broadcasting end or cannot fetch chat stream'''
|
||||||
raise exceptions.NoContents('Chat data stream is empty.')
|
self.raise_exception(exceptions.NoContents('Chat data stream is empty.'))
|
||||||
cont = contents['liveChatContinuation']['continuations'][0]
|
cont = contents['liveChatContinuation']['continuations'][0]
|
||||||
|
|
||||||
if cont.get("liveChatReplayContinuationData"):
|
if cont.get("liveChatReplayContinuationData"):
|
||||||
# chat data exist.
|
# chat data exist.
|
||||||
return None
|
return None
|
||||||
@@ -81,25 +94,29 @@ class Parser:
|
|||||||
init_cont = cont.get("playerSeekContinuationData")
|
init_cont = cont.get("playerSeekContinuationData")
|
||||||
if init_cont:
|
if init_cont:
|
||||||
return init_cont.get("continuation")
|
return init_cont.get("continuation")
|
||||||
raise exceptions.ChatDataFinished('Finished chat data')
|
self.raise_exception(exceptions.ChatDataFinished('Finished chat data'))
|
||||||
|
|
||||||
def _create_data(self, metadata, contents):
|
def _create_data(self, metadata, contents):
|
||||||
actions = contents['liveChatContinuation'].get('actions')
|
actions = contents['liveChatContinuation'].get('actions')
|
||||||
if self.is_replay:
|
if self.is_replay:
|
||||||
interval = self._get_interval(actions)
|
last_offset_ms = self._get_lastoffset(actions)
|
||||||
metadata.setdefault("timeoutMs", interval)
|
metadata.setdefault("timeoutMs", 5000)
|
||||||
|
metadata.setdefault("last_offset_ms", last_offset_ms)
|
||||||
"""Archived chat has different structures than live chat,
|
"""Archived chat has different structures than live chat,
|
||||||
so make it the same format."""
|
so make it the same format."""
|
||||||
chatdata = [action["replayChatItemAction"]["actions"][0]
|
chatdata = [action["replayChatItemAction"]["actions"][0]
|
||||||
for action in actions]
|
for action in actions]
|
||||||
else:
|
else:
|
||||||
metadata.setdefault('timeoutMs', 10000)
|
metadata.setdefault('timeoutMs', 5000)
|
||||||
chatdata = actions
|
chatdata = actions
|
||||||
return metadata, chatdata
|
return metadata, chatdata
|
||||||
|
|
||||||
def _get_interval(self, actions: list):
|
def _get_lastoffset(self, actions: list):
|
||||||
if actions is None:
|
if actions:
|
||||||
return 0
|
return int(actions[-1]["replayChatItemAction"]["videoOffsetTimeMsec"])
|
||||||
start = int(actions[0]["replayChatItemAction"]["videoOffsetTimeMsec"])
|
return 0
|
||||||
last = int(actions[-1]["replayChatItemAction"]["videoOffsetTimeMsec"])
|
|
||||||
return (last - start)
|
def raise_exception(self, exception):
|
||||||
|
if self.exception_holder is None:
|
||||||
|
raise exception
|
||||||
|
self.exception_holder = exception
|
||||||
|
|||||||
@@ -36,3 +36,7 @@ class Combinator(ChatProcessor):
|
|||||||
'''
|
'''
|
||||||
return tuple(processor.process(chat_components)
|
return tuple(processor.process(chat_components)
|
||||||
for processor in self.processors)
|
for processor in self.processors)
|
||||||
|
|
||||||
|
def finalize(self, *args, **kwargs):
|
||||||
|
[processor.finalize(*args, **kwargs)
|
||||||
|
for processor in self.processors]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import datetime
|
from datetime import datetime, timedelta, timezone
|
||||||
import pytz
|
|
||||||
|
TZ_UTC = timezone(timedelta(0), 'UTC')
|
||||||
|
|
||||||
|
|
||||||
class BaseRenderer:
|
class BaseRenderer:
|
||||||
@@ -62,13 +63,13 @@ class BaseRenderer:
|
|||||||
if badges:
|
if badges:
|
||||||
for badge in badges:
|
for badge in badges:
|
||||||
author_type = badge["liveChatAuthorBadgeRenderer"]["accessibility"]["accessibilityData"]["label"]
|
author_type = badge["liveChatAuthorBadgeRenderer"]["accessibility"]["accessibilityData"]["label"]
|
||||||
if author_type == '確認済み':
|
if author_type == 'VERIFIED' or author_type == '確認済み':
|
||||||
isVerified = True
|
isVerified = True
|
||||||
if author_type == '所有者':
|
if author_type == 'OWNER' or author_type == '所有者':
|
||||||
isChatOwner = True
|
isChatOwner = True
|
||||||
if 'メンバー' in author_type:
|
if 'メンバー' in author_type or 'MEMBER' in author_type:
|
||||||
isChatSponsor = True
|
isChatSponsor = True
|
||||||
if author_type == 'モデレーター':
|
if author_type == 'MODERATOR' or author_type == 'モデレーター':
|
||||||
isChatModerator = True
|
isChatModerator = True
|
||||||
return isVerified, isChatOwner, isChatSponsor, isChatModerator
|
return isVerified, isChatOwner, isChatSponsor, isChatModerator
|
||||||
|
|
||||||
@@ -76,6 +77,6 @@ class BaseRenderer:
|
|||||||
return self.renderer.get('id')
|
return self.renderer.get('id')
|
||||||
|
|
||||||
def get_publishedat(self, timestamp):
|
def get_publishedat(self, timestamp):
|
||||||
dt = datetime.datetime.fromtimestamp(int(timestamp) / 1000000)
|
dt = datetime.fromtimestamp(int(timestamp) / 1000000)
|
||||||
return dt.astimezone(pytz.utc).isoformat(
|
return dt.astimezone(TZ_UTC).isoformat(
|
||||||
timespec='milliseconds').replace('+00:00', 'Z')
|
timespec='milliseconds').replace('+00:00', 'Z')
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'''
|
'''
|
||||||
YouTubeスーパーチャットで使用される通貨の記号とレート検索用の略号の
|
Table of symbols for the currencies used in YouTube Superchat.
|
||||||
対応表
|
|
||||||
Key:
|
Key:
|
||||||
YouTubeスーパーチャットで使用される通貨の記号
|
Currency symbols used in YouTube Super Chat
|
||||||
(アルファベットで終わる場合、0xA0(&npsp)が付く)
|
If it ends with an alphabet, it will be followed by 0xA0(&npsp).
|
||||||
Value:
|
Value:
|
||||||
fxtext: 3文字の通貨略称
|
fxtext: ISO 4217 currency code
|
||||||
jptest: 日本語テキスト
|
jptest: japanese text
|
||||||
'''
|
'''
|
||||||
symbols = {
|
symbols = {
|
||||||
"$": {"fxtext": "USD", "jptext": "米・ドル"},
|
"$": {"fxtext": "USD", "jptext": "米・ドル"},
|
||||||
|
|||||||
11
pytchat/processors/default/custom_encoder.py
Normal file
11
pytchat/processors/default/custom_encoder.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import json
|
||||||
|
from .renderer.base import Author
|
||||||
|
from .renderer.paidmessage import Colors
|
||||||
|
from .renderer.paidsticker import Colors2
|
||||||
|
|
||||||
|
|
||||||
|
class CustomEncoder(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, Author) or isinstance(obj, Colors) or isinstance(obj, Colors2):
|
||||||
|
return vars(obj)
|
||||||
|
return json.JSONEncoder.default(self, obj)
|
||||||
@@ -1,35 +1,134 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
from .custom_encoder import CustomEncoder
|
||||||
from .renderer.textmessage import LiveChatTextMessageRenderer
|
from .renderer.textmessage import LiveChatTextMessageRenderer
|
||||||
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
||||||
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
||||||
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
|
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
|
||||||
from .renderer.membership import LiveChatMembershipItemRenderer
|
from .renderer.membership import LiveChatMembershipItemRenderer
|
||||||
|
from .renderer.donation import LiveChatDonationAnnouncementRenderer
|
||||||
from .. chat_processor import ChatProcessor
|
from .. chat_processor import ChatProcessor
|
||||||
from ... import config
|
from ... import config
|
||||||
|
|
||||||
logger = config.logger(__name__)
|
logger = config.logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Chat:
|
||||||
|
def json(self) -> str:
|
||||||
|
return json.dumps(vars(self), ensure_ascii=False, cls=CustomEncoder)
|
||||||
|
|
||||||
|
|
||||||
class Chatdata:
|
class Chatdata:
|
||||||
def __init__(self, chatlist: list, timeout: float):
|
|
||||||
|
def __init__(self, chatlist: list, timeout: float, abs_diff):
|
||||||
self.items = chatlist
|
self.items = chatlist
|
||||||
self.interval = timeout
|
self.interval = timeout
|
||||||
|
self.abs_diff = abs_diff
|
||||||
|
self.itemcount = 0
|
||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
if self.interval == 0:
|
'''DEPRECATE
|
||||||
|
Use sync_items()
|
||||||
|
'''
|
||||||
|
if len(self.items) < 1:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
return
|
return
|
||||||
time.sleep(self.interval / len(self.items))
|
if self.itemcount == 0:
|
||||||
|
self.starttime = time.time()
|
||||||
|
if len(self.items) == 1:
|
||||||
|
total_itemcount = 1
|
||||||
|
else:
|
||||||
|
total_itemcount = len(self.items) - 1
|
||||||
|
next_chattime = (self.items[0].timestamp + (self.items[-1].timestamp - self.items[0].timestamp) / total_itemcount * self.itemcount) / 1000
|
||||||
|
tobe_disptime = self.abs_diff + next_chattime
|
||||||
|
wait_sec = tobe_disptime - time.time()
|
||||||
|
self.itemcount += 1
|
||||||
|
|
||||||
|
if wait_sec < 0:
|
||||||
|
wait_sec = 0
|
||||||
|
|
||||||
|
time.sleep(wait_sec)
|
||||||
|
|
||||||
async def tick_async(self):
|
async def tick_async(self):
|
||||||
if self.interval == 0:
|
'''DEPRECATE
|
||||||
|
Use async_items()
|
||||||
|
'''
|
||||||
|
if len(self.items) < 1:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
return
|
return
|
||||||
await asyncio.sleep(self.interval / len(self.items))
|
if self.itemcount == 0:
|
||||||
|
self.starttime = time.time()
|
||||||
|
if len(self.items) == 1:
|
||||||
|
total_itemcount = 1
|
||||||
|
else:
|
||||||
|
total_itemcount = len(self.items) - 1
|
||||||
|
next_chattime = (self.items[0].timestamp + (self.items[-1].timestamp - self.items[0].timestamp) / total_itemcount * self.itemcount) / 1000
|
||||||
|
tobe_disptime = self.abs_diff + next_chattime
|
||||||
|
wait_sec = tobe_disptime - time.time()
|
||||||
|
self.itemcount += 1
|
||||||
|
|
||||||
|
if wait_sec < 0:
|
||||||
|
wait_sec = 0
|
||||||
|
|
||||||
|
await asyncio.sleep(wait_sec)
|
||||||
|
|
||||||
|
def sync_items(self):
|
||||||
|
starttime = time.time()
|
||||||
|
if len(self.items) > 0:
|
||||||
|
last_chattime = self.items[-1].timestamp / 1000
|
||||||
|
tobe_disptime = self.abs_diff + last_chattime
|
||||||
|
wait_total_sec = max(tobe_disptime - time.time(), 0)
|
||||||
|
if len(self.items) > 1:
|
||||||
|
wait_sec = wait_total_sec / len(self.items)
|
||||||
|
elif len(self.items) == 1:
|
||||||
|
wait_sec = 0
|
||||||
|
for c in self.items:
|
||||||
|
if wait_sec < 0:
|
||||||
|
wait_sec = 0
|
||||||
|
time.sleep(wait_sec)
|
||||||
|
yield c
|
||||||
|
stop_interval = time.time() - starttime
|
||||||
|
if stop_interval < 1:
|
||||||
|
time.sleep(1 - stop_interval)
|
||||||
|
|
||||||
|
async def async_items(self):
|
||||||
|
starttime = time.time()
|
||||||
|
if len(self.items) > 0:
|
||||||
|
last_chattime = self.items[-1].timestamp / 1000
|
||||||
|
tobe_disptime = self.abs_diff + last_chattime
|
||||||
|
wait_total_sec = max(tobe_disptime - time.time(), 0)
|
||||||
|
if len(self.items) > 1:
|
||||||
|
wait_sec = wait_total_sec / len(self.items)
|
||||||
|
elif len(self.items) == 1:
|
||||||
|
wait_sec = 0
|
||||||
|
for c in self.items:
|
||||||
|
if wait_sec < 0:
|
||||||
|
wait_sec = 0
|
||||||
|
await asyncio.sleep(wait_sec)
|
||||||
|
yield c
|
||||||
|
|
||||||
|
stop_interval = time.time() - starttime
|
||||||
|
if stop_interval < 1:
|
||||||
|
await asyncio.sleep(1 - stop_interval)
|
||||||
|
|
||||||
|
def json(self) -> str:
|
||||||
|
return ''.join(("[", ','.join((a.json() for a in self.items)), "]"))
|
||||||
|
|
||||||
|
|
||||||
class DefaultProcessor(ChatProcessor):
|
class DefaultProcessor(ChatProcessor):
|
||||||
|
def __init__(self):
|
||||||
|
self.first = True
|
||||||
|
self.abs_diff = 0
|
||||||
|
self.renderers = {
|
||||||
|
"liveChatTextMessageRenderer": LiveChatTextMessageRenderer(),
|
||||||
|
"liveChatPaidMessageRenderer": LiveChatPaidMessageRenderer(),
|
||||||
|
"liveChatPaidStickerRenderer": LiveChatPaidStickerRenderer(),
|
||||||
|
"liveChatLegacyPaidMessageRenderer": LiveChatLegacyPaidMessageRenderer(),
|
||||||
|
"liveChatMembershipItemRenderer": LiveChatMembershipItemRenderer(),
|
||||||
|
"liveChatDonationAnnouncementRenderer": LiveChatDonationAnnouncementRenderer(),
|
||||||
|
}
|
||||||
|
|
||||||
def process(self, chat_components: list):
|
def process(self, chat_components: list):
|
||||||
|
|
||||||
chatlist = []
|
chatlist = []
|
||||||
@@ -37,8 +136,10 @@ class DefaultProcessor(ChatProcessor):
|
|||||||
|
|
||||||
if chat_components:
|
if chat_components:
|
||||||
for component in chat_components:
|
for component in chat_components:
|
||||||
|
if component is None:
|
||||||
|
continue
|
||||||
timeout += component.get('timeout', 0)
|
timeout += component.get('timeout', 0)
|
||||||
chatdata = component.get('chatdata')
|
chatdata = component.get('chatdata') # if from Extractor, chatdata is generator.
|
||||||
if chatdata is None:
|
if chatdata is None:
|
||||||
continue
|
continue
|
||||||
for action in chatdata:
|
for action in chatdata:
|
||||||
@@ -46,43 +147,35 @@ class DefaultProcessor(ChatProcessor):
|
|||||||
continue
|
continue
|
||||||
if action.get('addChatItemAction') is None:
|
if action.get('addChatItemAction') is None:
|
||||||
continue
|
continue
|
||||||
if action['addChatItemAction'].get('item') is None:
|
item = action['addChatItemAction'].get('item')
|
||||||
|
if item is None:
|
||||||
continue
|
continue
|
||||||
|
chat = self._parse(item)
|
||||||
chat = self._parse(action)
|
|
||||||
if chat:
|
if chat:
|
||||||
chatlist.append(chat)
|
chatlist.append(chat)
|
||||||
return Chatdata(chatlist, float(timeout))
|
|
||||||
|
if self.first and chatlist:
|
||||||
|
self.abs_diff = time.time() - chatlist[0].timestamp / 1000
|
||||||
|
self.first = False
|
||||||
|
|
||||||
def _parse(self, sitem):
|
chatdata = Chatdata(chatlist, float(timeout), self.abs_diff)
|
||||||
action = sitem.get("addChatItemAction")
|
|
||||||
if action:
|
return chatdata
|
||||||
item = action.get("item")
|
|
||||||
if item is None:
|
def _parse(self, item):
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
renderer = self._get_renderer(item)
|
key = list(item.keys())[0]
|
||||||
|
renderer = self.renderers.get(key)
|
||||||
if renderer is None:
|
if renderer is None:
|
||||||
return None
|
return None
|
||||||
|
renderer.setitem(item.get(key), Chat())
|
||||||
|
renderer.settype()
|
||||||
renderer.get_snippet()
|
renderer.get_snippet()
|
||||||
renderer.get_authordetails()
|
renderer.get_authordetails()
|
||||||
|
rendered_chatobj = renderer.get_chatobj()
|
||||||
|
renderer.clear()
|
||||||
except (KeyError, TypeError) as e:
|
except (KeyError, TypeError) as e:
|
||||||
logger.error(f"{str(type(e))}-{str(e)} sitem:{str(sitem)}")
|
logger.error(f"{str(type(e))}-{str(e)} item:{str(item)}")
|
||||||
return None
|
return None
|
||||||
return renderer
|
|
||||||
|
return rendered_chatobj
|
||||||
def _get_renderer(self, item):
|
|
||||||
if item.get("liveChatTextMessageRenderer"):
|
|
||||||
renderer = LiveChatTextMessageRenderer(item)
|
|
||||||
elif item.get("liveChatPaidMessageRenderer"):
|
|
||||||
renderer = LiveChatPaidMessageRenderer(item)
|
|
||||||
elif item.get("liveChatPaidStickerRenderer"):
|
|
||||||
renderer = LiveChatPaidStickerRenderer(item)
|
|
||||||
elif item.get("liveChatLegacyPaidMessageRenderer"):
|
|
||||||
renderer = LiveChatLegacyPaidMessageRenderer(item)
|
|
||||||
elif item.get("liveChatMembershipItemRenderer"):
|
|
||||||
renderer = LiveChatMembershipItemRenderer(item)
|
|
||||||
else:
|
|
||||||
renderer = None
|
|
||||||
return renderer
|
|
||||||
|
|||||||
@@ -6,89 +6,96 @@ class Author:
|
|||||||
|
|
||||||
|
|
||||||
class BaseRenderer:
|
class BaseRenderer:
|
||||||
def __init__(self, item, chattype):
|
def setitem(self, item, chat):
|
||||||
self.renderer = list(item.values())[0]
|
self.item = item
|
||||||
self.chattype = chattype
|
self.chat = chat
|
||||||
self.author = Author()
|
self.chat.author = Author()
|
||||||
|
|
||||||
|
def settype(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def get_snippet(self):
|
def get_snippet(self):
|
||||||
self.type = self.chattype
|
self.chat.id = self.item.get('id')
|
||||||
self.id = self.renderer.get('id')
|
timestampUsec = int(self.item.get("timestampUsec", 0))
|
||||||
timestampUsec = int(self.renderer.get("timestampUsec", 0))
|
self.chat.timestamp = int(timestampUsec / 1000)
|
||||||
self.timestamp = int(timestampUsec / 1000)
|
tst = self.item.get("timestampText")
|
||||||
tst = self.renderer.get("timestampText")
|
|
||||||
if tst:
|
if tst:
|
||||||
self.elapsedTime = tst.get("simpleText")
|
self.chat.elapsedTime = tst.get("simpleText")
|
||||||
else:
|
else:
|
||||||
self.elapsedTime = ""
|
self.chat.elapsedTime = ""
|
||||||
self.datetime = self.get_datetime(timestampUsec)
|
self.chat.datetime = self.get_datetime(timestampUsec)
|
||||||
self.message, self.messageEx = self.get_message(self.renderer)
|
self.chat.message, self.chat.messageEx = self.get_message(self.item)
|
||||||
self.id = self.renderer.get('id')
|
self.chat.id = self.item.get('id')
|
||||||
self.amountValue = 0.0
|
self.chat.amountValue = 0.0
|
||||||
self.amountString = ""
|
self.chat.amountString = ""
|
||||||
self.currency = ""
|
self.chat.currency = ""
|
||||||
self.bgColor = 0
|
self.chat.bgColor = 0
|
||||||
|
|
||||||
def get_authordetails(self):
|
def get_authordetails(self):
|
||||||
self.author.badgeUrl = ""
|
self.chat.author.badgeUrl = ""
|
||||||
(self.author.isVerified,
|
(self.chat.author.isVerified,
|
||||||
self.author.isChatOwner,
|
self.chat.author.isChatOwner,
|
||||||
self.author.isChatSponsor,
|
self.chat.author.isChatSponsor,
|
||||||
self.author.isChatModerator) = (
|
self.chat.author.isChatModerator) = (
|
||||||
self.get_badges(self.renderer)
|
self.get_badges(self.item)
|
||||||
)
|
)
|
||||||
self.author.channelId = self.renderer.get("authorExternalChannelId")
|
self.chat.author.channelId = self.item.get("authorExternalChannelId")
|
||||||
self.author.channelUrl = "http://www.youtube.com/channel/" + self.author.channelId
|
self.chat.author.channelUrl = "http://www.youtube.com/channel/" + self.chat.author.channelId
|
||||||
self.author.name = self.renderer["authorName"]["simpleText"]
|
self.chat.author.name = self.item["authorName"]["simpleText"]
|
||||||
self.author.imageUrl = self.renderer["authorPhoto"]["thumbnails"][1]["url"]
|
self.chat.author.imageUrl = self.item["authorPhoto"]["thumbnails"][1]["url"]
|
||||||
|
|
||||||
def get_message(self, renderer):
|
def get_message(self, item):
|
||||||
message = ''
|
message = ''
|
||||||
message_ex = []
|
message_ex = []
|
||||||
if renderer.get("message"):
|
runs = item.get("message", {}).get("runs", {})
|
||||||
runs = renderer["message"].get("runs")
|
for r in runs:
|
||||||
if runs:
|
if not hasattr(r, "get"):
|
||||||
for r in runs:
|
continue
|
||||||
if r:
|
if r.get('emoji'):
|
||||||
if r.get('emoji'):
|
message += r['emoji'].get('shortcuts', [''])[0]
|
||||||
message += r['emoji'].get('shortcuts', [''])[0]
|
message_ex.append({
|
||||||
message_ex.append({
|
'id': r['emoji'].get('emojiId').split('/')[-1],
|
||||||
'id': r['emoji'].get('emojiId').split('/')[-1],
|
'txt': r['emoji'].get('shortcuts', [''])[0],
|
||||||
'txt': r['emoji'].get('shortcuts', [''])[0],
|
'url': r['emoji']['image']['thumbnails'][0].get('url')
|
||||||
'url': r['emoji']['image']['thumbnails'][0].get('url')
|
})
|
||||||
})
|
else:
|
||||||
else:
|
message += r.get('text', '')
|
||||||
message += r.get('text', '')
|
message_ex.append(r.get('text', ''))
|
||||||
message_ex.append(r.get('text', ''))
|
|
||||||
return message, message_ex
|
return message, message_ex
|
||||||
|
|
||||||
def get_badges(self, renderer):
|
def get_badges(self, renderer):
|
||||||
self.author.type = ''
|
self.chat.author.type = ''
|
||||||
isVerified = False
|
isVerified = False
|
||||||
isChatOwner = False
|
isChatOwner = False
|
||||||
isChatSponsor = False
|
isChatSponsor = False
|
||||||
isChatModerator = False
|
isChatModerator = False
|
||||||
badges = renderer.get("authorBadges")
|
badges = renderer.get("authorBadges", {})
|
||||||
if badges:
|
for badge in badges:
|
||||||
for badge in badges:
|
if badge["liveChatAuthorBadgeRenderer"].get("icon"):
|
||||||
if badge["liveChatAuthorBadgeRenderer"].get("icon"):
|
author_type = badge["liveChatAuthorBadgeRenderer"]["icon"]["iconType"]
|
||||||
author_type = badge["liveChatAuthorBadgeRenderer"]["icon"]["iconType"]
|
self.chat.author.type = author_type
|
||||||
self.author.type = author_type
|
if author_type == 'VERIFIED':
|
||||||
if author_type == 'VERIFIED':
|
isVerified = True
|
||||||
isVerified = True
|
if author_type == 'OWNER':
|
||||||
if author_type == 'OWNER':
|
isChatOwner = True
|
||||||
isChatOwner = True
|
if author_type == 'MODERATOR':
|
||||||
if author_type == 'MODERATOR':
|
isChatModerator = True
|
||||||
isChatModerator = True
|
if badge["liveChatAuthorBadgeRenderer"].get("customThumbnail"):
|
||||||
if badge["liveChatAuthorBadgeRenderer"].get("customThumbnail"):
|
isChatSponsor = True
|
||||||
isChatSponsor = True
|
self.chat.author.type = 'MEMBER'
|
||||||
self.author.type = 'MEMBER'
|
self.get_badgeurl(badge)
|
||||||
self.get_badgeurl(badge)
|
|
||||||
return isVerified, isChatOwner, isChatSponsor, isChatModerator
|
return isVerified, isChatOwner, isChatSponsor, isChatModerator
|
||||||
|
|
||||||
def get_badgeurl(self, badge):
|
def get_badgeurl(self, badge):
|
||||||
self.author.badgeUrl = badge["liveChatAuthorBadgeRenderer"]["customThumbnail"]["thumbnails"][0]["url"]
|
self.chat.author.badgeUrl = badge["liveChatAuthorBadgeRenderer"]["customThumbnail"]["thumbnails"][0]["url"]
|
||||||
|
|
||||||
def get_datetime(self, timestamp):
|
def get_datetime(self, timestamp):
|
||||||
dt = datetime.fromtimestamp(timestamp / 1000000)
|
dt = datetime.fromtimestamp(timestamp / 1000000)
|
||||||
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
def get_chatobj(self):
|
||||||
|
return self.chat
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.item = None
|
||||||
|
self.chat = None
|
||||||
|
|||||||
6
pytchat/processors/default/renderer/donation.py
Normal file
6
pytchat/processors/default/renderer/donation.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from .base import BaseRenderer
|
||||||
|
|
||||||
|
|
||||||
|
class LiveChatDonationAnnouncementRenderer(BaseRenderer):
|
||||||
|
def settype(self):
|
||||||
|
self.chat.type = "donation"
|
||||||
@@ -2,14 +2,14 @@ from .base import BaseRenderer
|
|||||||
|
|
||||||
|
|
||||||
class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
|
class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
|
||||||
def __init__(self, item):
|
def settype(self):
|
||||||
super().__init__(item, "newSponsor")
|
self.chat.type = "newSponsor"
|
||||||
|
|
||||||
def get_authordetails(self):
|
def get_authordetails(self):
|
||||||
super().get_authordetails()
|
super().get_authordetails()
|
||||||
self.author.isChatSponsor = True
|
self.chat.author.isChatSponsor = True
|
||||||
|
|
||||||
def get_message(self, renderer):
|
def get_message(self, item):
|
||||||
message = (renderer["eventText"]["runs"][0]["text"]
|
message = (item["eventText"]["runs"][0]["text"]
|
||||||
) + ' / ' + (renderer["detailText"]["simpleText"])
|
) + ' / ' + (item["detailText"]["simpleText"])
|
||||||
return message, [message]
|
return message, [message]
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ from .base import BaseRenderer
|
|||||||
|
|
||||||
|
|
||||||
class LiveChatMembershipItemRenderer(BaseRenderer):
|
class LiveChatMembershipItemRenderer(BaseRenderer):
|
||||||
def __init__(self, item):
|
def settype(self):
|
||||||
super().__init__(item, "newSponsor")
|
self.chat.type = "newSponsor"
|
||||||
|
|
||||||
def get_authordetails(self):
|
def get_authordetails(self):
|
||||||
super().get_authordetails()
|
super().get_authordetails()
|
||||||
self.author.isChatSponsor = True
|
self.chat.author.isChatSponsor = True
|
||||||
|
|
||||||
def get_message(self, renderer):
|
def get_message(self, item):
|
||||||
message = ''.join([mes.get("text", "")
|
try:
|
||||||
for mes in renderer["headerSubtext"]["runs"]])
|
message = ''.join([mes.get("text", "")
|
||||||
|
for mes in item["headerSubtext"]["runs"]])
|
||||||
|
except KeyError:
|
||||||
|
return "Welcome New Member!", ["Welcome New Member!"]
|
||||||
return message, [message]
|
return message, [message]
|
||||||
|
|||||||
@@ -9,23 +9,23 @@ class Colors:
|
|||||||
|
|
||||||
|
|
||||||
class LiveChatPaidMessageRenderer(BaseRenderer):
|
class LiveChatPaidMessageRenderer(BaseRenderer):
|
||||||
def __init__(self, item):
|
def settype(self):
|
||||||
super().__init__(item, "superChat")
|
self.chat.type = "superChat"
|
||||||
|
|
||||||
def get_snippet(self):
|
def get_snippet(self):
|
||||||
super().get_snippet()
|
super().get_snippet()
|
||||||
amountDisplayString, symbol, amount = (
|
amountDisplayString, symbol, amount = (
|
||||||
self.get_amountdata(self.renderer)
|
self.get_amountdata(self.item)
|
||||||
)
|
)
|
||||||
self.amountValue = amount
|
self.chat.amountValue = amount
|
||||||
self.amountString = amountDisplayString
|
self.chat.amountString = amountDisplayString
|
||||||
self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
|
self.chat.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
|
||||||
symbol) else symbol
|
symbol) else symbol
|
||||||
self.bgColor = self.renderer.get("bodyBackgroundColor", 0)
|
self.chat.bgColor = self.item.get("bodyBackgroundColor", 0)
|
||||||
self.colors = self.get_colors()
|
self.chat.colors = self.get_colors()
|
||||||
|
|
||||||
def get_amountdata(self, renderer):
|
def get_amountdata(self, item):
|
||||||
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
|
amountDisplayString = item["purchaseAmountText"]["simpleText"]
|
||||||
m = superchat_regex.search(amountDisplayString)
|
m = superchat_regex.search(amountDisplayString)
|
||||||
if m:
|
if m:
|
||||||
symbol = m.group(1)
|
symbol = m.group(1)
|
||||||
@@ -36,11 +36,12 @@ class LiveChatPaidMessageRenderer(BaseRenderer):
|
|||||||
return amountDisplayString, symbol, amount
|
return amountDisplayString, symbol, amount
|
||||||
|
|
||||||
def get_colors(self):
|
def get_colors(self):
|
||||||
|
item = self.item
|
||||||
colors = Colors()
|
colors = Colors()
|
||||||
colors.headerBackgroundColor = self.renderer.get("headerBackgroundColor", 0)
|
colors.headerBackgroundColor = item.get("headerBackgroundColor", 0)
|
||||||
colors.headerTextColor = self.renderer.get("headerTextColor", 0)
|
colors.headerTextColor = item.get("headerTextColor", 0)
|
||||||
colors.bodyBackgroundColor = self.renderer.get("bodyBackgroundColor", 0)
|
colors.bodyBackgroundColor = item.get("bodyBackgroundColor", 0)
|
||||||
colors.bodyTextColor = self.renderer.get("bodyTextColor", 0)
|
colors.bodyTextColor = item.get("bodyTextColor", 0)
|
||||||
colors.timestampColor = self.renderer.get("timestampColor", 0)
|
colors.timestampColor = item.get("timestampColor", 0)
|
||||||
colors.authorNameTextColor = self.renderer.get("authorNameTextColor", 0)
|
colors.authorNameTextColor = item.get("authorNameTextColor", 0)
|
||||||
return colors
|
return colors
|
||||||
|
|||||||
@@ -4,30 +4,30 @@ from .base import BaseRenderer
|
|||||||
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
|
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
|
||||||
|
|
||||||
|
|
||||||
class Colors:
|
class Colors2:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LiveChatPaidStickerRenderer(BaseRenderer):
|
class LiveChatPaidStickerRenderer(BaseRenderer):
|
||||||
def __init__(self, item):
|
def settype(self):
|
||||||
super().__init__(item, "superSticker")
|
self.chat.type = "superSticker"
|
||||||
|
|
||||||
def get_snippet(self):
|
def get_snippet(self):
|
||||||
super().get_snippet()
|
super().get_snippet()
|
||||||
amountDisplayString, symbol, amount = (
|
amountDisplayString, symbol, amount = (
|
||||||
self.get_amountdata(self.renderer)
|
self.get_amountdata(self.item)
|
||||||
)
|
)
|
||||||
self.amountValue = amount
|
self.chat.amountValue = amount
|
||||||
self.amountString = amountDisplayString
|
self.chat.amountString = amountDisplayString
|
||||||
self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
|
self.chat.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
|
||||||
symbol) else symbol
|
symbol) else symbol
|
||||||
self.bgColor = self.renderer.get("backgroundColor", 0)
|
self.chat.bgColor = self.item.get("backgroundColor", 0)
|
||||||
self.sticker = "".join(("https:",
|
self.chat.sticker = "".join(("https:",
|
||||||
self.renderer["sticker"]["thumbnails"][0]["url"]))
|
self.item["sticker"]["thumbnails"][0]["url"]))
|
||||||
self.colors = self.get_colors()
|
self.chat.colors = self.get_colors()
|
||||||
|
|
||||||
def get_amountdata(self, renderer):
|
def get_amountdata(self, item):
|
||||||
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
|
amountDisplayString = item["purchaseAmountText"]["simpleText"]
|
||||||
m = superchat_regex.search(amountDisplayString)
|
m = superchat_regex.search(amountDisplayString)
|
||||||
if m:
|
if m:
|
||||||
symbol = m.group(1)
|
symbol = m.group(1)
|
||||||
@@ -38,9 +38,10 @@ class LiveChatPaidStickerRenderer(BaseRenderer):
|
|||||||
return amountDisplayString, symbol, amount
|
return amountDisplayString, symbol, amount
|
||||||
|
|
||||||
def get_colors(self):
|
def get_colors(self):
|
||||||
colors = Colors()
|
item = self.item
|
||||||
colors.moneyChipBackgroundColor = self.renderer.get("moneyChipBackgroundColor", 0)
|
colors = Colors2()
|
||||||
colors.moneyChipTextColor = self.renderer.get("moneyChipTextColor", 0)
|
colors.moneyChipBackgroundColor = item.get("moneyChipBackgroundColor", 0)
|
||||||
colors.backgroundColor = self.renderer.get("backgroundColor", 0)
|
colors.moneyChipTextColor = item.get("moneyChipTextColor", 0)
|
||||||
colors.authorNameTextColor = self.renderer.get("authorNameTextColor", 0)
|
colors.backgroundColor = item.get("backgroundColor", 0)
|
||||||
|
colors.authorNameTextColor = item.get("authorNameTextColor", 0)
|
||||||
return colors
|
return colors
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ from .base import BaseRenderer
|
|||||||
|
|
||||||
|
|
||||||
class LiveChatTextMessageRenderer(BaseRenderer):
|
class LiveChatTextMessageRenderer(BaseRenderer):
|
||||||
def __init__(self, item):
|
def settype(self):
|
||||||
super().__init__(item, "textMessage")
|
self.chat.type = "textMessage"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import httpx
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import httpx
|
import time
|
||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from .chat_processor import ChatProcessor
|
from .chat_processor import ChatProcessor
|
||||||
from .default.processor import DefaultProcessor
|
from .default.processor import DefaultProcessor
|
||||||
|
from ..exceptions import UnknownConnectionError
|
||||||
|
import tempfile
|
||||||
|
|
||||||
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
|
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
|
||||||
|
|
||||||
@@ -43,14 +46,17 @@ class HTMLArchiver(ChatProcessor):
|
|||||||
'''
|
'''
|
||||||
HTMLArchiver saves chat data as HTML table format.
|
HTMLArchiver saves chat data as HTML table format.
|
||||||
'''
|
'''
|
||||||
def __init__(self, save_path, callback):
|
def __init__(self, save_path, callback=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.client = httpx.Client(http2=True)
|
||||||
self.save_path = self._checkpath(save_path)
|
self.save_path = self._checkpath(save_path)
|
||||||
self.processor = DefaultProcessor()
|
self.processor = DefaultProcessor()
|
||||||
self.emoji_table = {} # tuble for custom emojis. key: emoji_id, value: base64 encoded image binary.
|
self.emoji_table = {} # dict for custom emojis. key: emoji_id, value: base64 encoded image binary.
|
||||||
self.header = [HEADER_HTML]
|
|
||||||
self.body = ['<body>\n', '<table class="css">\n', self._parse_table_header(fmt_headers)]
|
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
self.executor = ThreadPoolExecutor(max_workers=10)
|
||||||
|
self.tmp_fp = tempfile.NamedTemporaryFile(mode="a", encoding="utf-8", delete=False)
|
||||||
|
self.tmp_filename = self.tmp_fp.name
|
||||||
|
self.counter = 0
|
||||||
|
|
||||||
def _checkpath(self, filepath):
|
def _checkpath(self, filepath):
|
||||||
splitter = os.path.splitext(os.path.basename(filepath))
|
splitter = os.path.splitext(os.path.basename(filepath))
|
||||||
@@ -77,12 +83,12 @@ class HTMLArchiver(ChatProcessor):
|
|||||||
save_path : str :
|
save_path : str :
|
||||||
Actual save path of file.
|
Actual save path of file.
|
||||||
total_lines : int :
|
total_lines : int :
|
||||||
count of total lines written to the file.
|
Count of total lines written to the file.
|
||||||
"""
|
"""
|
||||||
if chat_components is None or len(chat_components) == 0:
|
if chat_components is None or len(chat_components) == 0:
|
||||||
return
|
return self.save_path, self.counter
|
||||||
for c in self.processor.process(chat_components).items:
|
for c in self.processor.process(chat_components).items:
|
||||||
self.body.extend(
|
self.tmp_fp.write(
|
||||||
self._parse_html_line((
|
self._parse_html_line((
|
||||||
c.datetime,
|
c.datetime,
|
||||||
c.elapsedTime,
|
c.elapsedTime,
|
||||||
@@ -93,7 +99,10 @@ class HTMLArchiver(ChatProcessor):
|
|||||||
c.author.channelId)
|
c.author.channelId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.callback(None, 1)
|
if self.callback:
|
||||||
|
self.callback(None, 1)
|
||||||
|
self.counter += 1
|
||||||
|
return self.save_path, self.counter
|
||||||
|
|
||||||
def _parse_html_line(self, raw_line):
|
def _parse_html_line(self, raw_line):
|
||||||
return ''.join(('<tr>',
|
return ''.join(('<tr>',
|
||||||
@@ -111,13 +120,23 @@ class HTMLArchiver(ChatProcessor):
|
|||||||
for item in message_items)
|
for item in message_items)
|
||||||
|
|
||||||
def _encode_img(self, url):
|
def _encode_img(self, url):
|
||||||
resp = httpx.get(url)
|
err = None
|
||||||
|
for _ in range(5):
|
||||||
|
try:
|
||||||
|
resp = self.client.get(url, timeout=30)
|
||||||
|
break
|
||||||
|
except httpx.HTTPError as e:
|
||||||
|
err = e
|
||||||
|
time.sleep(3)
|
||||||
|
else:
|
||||||
|
raise UnknownConnectionError(str(err))
|
||||||
|
|
||||||
return standard_b64encode(resp.content).decode()
|
return standard_b64encode(resp.content).decode()
|
||||||
|
|
||||||
def _set_emoji_table(self, item: dict):
|
def _set_emoji_table(self, item: dict):
|
||||||
emoji_id = item['id']
|
emoji_id = ''.join(('Z', item['id'])) if 48 <= ord(item['id'][0]) <= 57 else item['id']
|
||||||
if emoji_id not in self.emoji_table:
|
if emoji_id not in self.emoji_table:
|
||||||
self.emoji_table.setdefault(emoji_id, self._encode_img(item['url']))
|
self.emoji_table.setdefault(emoji_id, self.executor.submit(self._encode_img, item['url']))
|
||||||
return emoji_id
|
return emoji_id
|
||||||
|
|
||||||
def _stylecode(self, name, code, width, height):
|
def _stylecode(self, name, code, width, height):
|
||||||
@@ -128,13 +147,24 @@ class HTMLArchiver(ChatProcessor):
|
|||||||
def _create_styles(self):
|
def _create_styles(self):
|
||||||
return '\n'.join(('<style type="text/css">',
|
return '\n'.join(('<style type="text/css">',
|
||||||
TABLE_CSS,
|
TABLE_CSS,
|
||||||
'\n'.join(self._stylecode(key, self.emoji_table[key], 24, 24)
|
'\n'.join(self._stylecode(key, self.emoji_table[key].result(), 24, 24)
|
||||||
for key in self.emoji_table.keys()),
|
for key in self.emoji_table.keys()),
|
||||||
'</style>\n'))
|
'</style>\n'))
|
||||||
|
|
||||||
def finalize(self):
|
def finalize(self):
|
||||||
self.header.extend([self._create_styles(), '</head>\n'])
|
if self.tmp_fp:
|
||||||
self.body.extend(['</table>\n</body>\n</html>'])
|
self.tmp_fp.flush()
|
||||||
with open(self.save_path, mode='a', encoding='utf-8') as f:
|
self.tmp_fp = None
|
||||||
f.writelines(self.header)
|
with open(self.save_path, mode='w', encoding='utf-8') as outfile:
|
||||||
f.writelines(self.body)
|
# write header
|
||||||
|
outfile.writelines((
|
||||||
|
HEADER_HTML, self._create_styles(), '</head>\n',
|
||||||
|
'<body>\n', '<table class="css">\n',
|
||||||
|
self._parse_table_header(fmt_headers)))
|
||||||
|
# write body
|
||||||
|
fp = open(self.tmp_filename, mode="r", encoding="utf-8")
|
||||||
|
for line in fp:
|
||||||
|
outfile.write(line)
|
||||||
|
outfile.write('</table>\n</body>\n</html>')
|
||||||
|
fp.close()
|
||||||
|
os.remove(self.tmp_filename)
|
||||||
|
|||||||
@@ -82,16 +82,17 @@ class RingQueue:
|
|||||||
|
|
||||||
class SpeedCalculator(ChatProcessor, RingQueue):
|
class SpeedCalculator(ChatProcessor, RingQueue):
|
||||||
"""
|
"""
|
||||||
チャットの勢いを計算する。
|
Calculate the momentum of the chat.
|
||||||
|
|
||||||
一定期間のチャットデータのうち、最初のチャットの投稿時刻と
|
Divide the difference between the time of the first chat and
|
||||||
最後のチャットの投稿時刻の差を、チャット数で割り返し
|
the time of the last chat in the chat data over a period of
|
||||||
1分あたりの速度に換算する。
|
time by the number of chats and convert it to speed per minute.
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
----------
|
----------
|
||||||
capacity : int
|
capacity : int
|
||||||
RingQueueに格納するチャット勢い算出用データの最大数
|
Maximum number of data for calculating chat momentum
|
||||||
|
to be stored in RingQueue.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, capacity=10):
|
def __init__(self, capacity=10):
|
||||||
@@ -111,17 +112,17 @@ class SpeedCalculator(ChatProcessor, RingQueue):
|
|||||||
|
|
||||||
def _calc_speed(self):
|
def _calc_speed(self):
|
||||||
"""
|
"""
|
||||||
RingQueue内のチャット勢い算出用データリストを元に、
|
Calculates the chat speed based on the data list for calculating
|
||||||
チャット速度を計算して返す
|
the chat momentum in RingQueue.
|
||||||
|
|
||||||
Return
|
Return
|
||||||
---------------------------
|
---------------------------
|
||||||
チャット速度(1分間で換算したチャット数)
|
Chat speed (number of chats converted per minute)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# キュー内の総チャット数
|
# Total number of chats in the queue
|
||||||
total = sum(item['chat_count'] for item in self.items)
|
total = sum(item['chat_count'] for item in self.items)
|
||||||
# キュー内の最初と最後のチャットの時間差
|
# Interval between the first and last chats in the queue
|
||||||
duration = (self.items[self.last_pos]['endtime'] - self.items[self.first_pos]['starttime'])
|
duration = (self.items[self.last_pos]['endtime'] - self.items[self.first_pos]['starttime'])
|
||||||
if duration != 0:
|
if duration != 0:
|
||||||
return int(total * 60 / duration)
|
return int(total * 60 / duration)
|
||||||
@@ -131,19 +132,12 @@ class SpeedCalculator(ChatProcessor, RingQueue):
|
|||||||
|
|
||||||
def _put_chatdata(self, actions):
|
def _put_chatdata(self, actions):
|
||||||
"""
|
"""
|
||||||
チャットデータからタイムスタンプを読み取り、勢い測定用のデータを組み立て、
|
|
||||||
RingQueueに投入する。
|
|
||||||
200円以上のスパチャはtickerとmessageの2つのデータが生成されるが、
|
|
||||||
tickerの方は時刻データの場所が異なることを利用し、勢いの集計から除外している。
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
actions : List[dict]
|
actions : List[dict]
|
||||||
チャットデータ(addChatItemAction) のリスト
|
List of addChatItemActions
|
||||||
"""
|
"""
|
||||||
def _put_emptydata():
|
def _put_emptydata():
|
||||||
'''
|
|
||||||
チャットデータがない場合に空のデータをキューに投入する。
|
|
||||||
'''
|
|
||||||
timestamp_now = int(time.time())
|
timestamp_now = int(time.time())
|
||||||
self.put({
|
self.put({
|
||||||
'chat_count': 0,
|
'chat_count': 0,
|
||||||
@@ -152,9 +146,6 @@ class SpeedCalculator(ChatProcessor, RingQueue):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def _get_timestamp(action: dict):
|
def _get_timestamp(action: dict):
|
||||||
"""
|
|
||||||
チャットデータから時刻データを取り出す。
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
item = action['addChatItemAction']['item']
|
item = action['addChatItemAction']['item']
|
||||||
timestamp = int(item[list(item.keys())[0]]['timestampUsec'])
|
timestamp = int(item[list(item.keys())[0]]['timestampUsec'])
|
||||||
@@ -166,32 +157,24 @@ class SpeedCalculator(ChatProcessor, RingQueue):
|
|||||||
_put_emptydata()
|
_put_emptydata()
|
||||||
return
|
return
|
||||||
|
|
||||||
# actions内の時刻データを持つチャットデータの数
|
|
||||||
counter = 0
|
counter = 0
|
||||||
# actions内の最初のチャットデータの時刻
|
|
||||||
starttime = None
|
starttime = None
|
||||||
# actions内の最後のチャットデータの時刻
|
|
||||||
endtime = None
|
endtime = None
|
||||||
|
|
||||||
for action in actions:
|
for action in actions:
|
||||||
# チャットデータからtimestampUsecを読み取る
|
# Get timestampUsec from chatdata
|
||||||
gettime = _get_timestamp(action)
|
gettime = _get_timestamp(action)
|
||||||
|
|
||||||
# 時刻のないデータだった場合は次の行のデータで読み取り試行
|
|
||||||
if gettime is None:
|
if gettime is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 最初に有効な時刻を持つデータのtimestampをstarttimeに設定
|
|
||||||
if starttime is None:
|
if starttime is None:
|
||||||
starttime = gettime
|
starttime = gettime
|
||||||
|
|
||||||
# 最後のtimestampを設定(途中で時刻のないデータの場合もあるので上書きしていく)
|
|
||||||
endtime = gettime
|
endtime = gettime
|
||||||
|
|
||||||
# チャットの数をインクリメント
|
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
# チャット速度用のデータをRingQueueに送る
|
|
||||||
if starttime is None or endtime is None:
|
if starttime is None or endtime is None:
|
||||||
_put_emptydata()
|
_put_emptydata()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
import httpx
|
|
||||||
import asyncio
|
|
||||||
from . import parser
|
|
||||||
from . block import Block
|
|
||||||
from . worker import ExtractWorker
|
|
||||||
from . patch import Patch
|
|
||||||
from ... import config
|
|
||||||
from ... paramgen import arcparam
|
|
||||||
from ... exceptions import UnknownConnectionError
|
|
||||||
from concurrent.futures import CancelledError
|
|
||||||
from json import JSONDecodeError
|
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
headers = config.headers
|
|
||||||
REPLAY_URL = "https://www.youtube.com/live_chat_replay/" \
|
|
||||||
"get_live_chat_replay?continuation="
|
|
||||||
MAX_RETRY_COUNT = 3
|
|
||||||
|
|
||||||
|
|
||||||
def _split(start, end, count, min_interval_sec=120):
|
|
||||||
"""
|
|
||||||
Split section from `start` to `end` into `count` pieces,
|
|
||||||
and returns the beginning of each piece.
|
|
||||||
The `count` is adjusted so that the length of each piece
|
|
||||||
is no smaller than `min_interval`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
--------
|
|
||||||
List of the offset of each block's first chat data.
|
|
||||||
"""
|
|
||||||
if not (isinstance(start, int) or isinstance(start, float)) or \
|
|
||||||
not (isinstance(end, int) or isinstance(end, float)):
|
|
||||||
raise ValueError("start/end must be int or float")
|
|
||||||
if not isinstance(count, int):
|
|
||||||
raise ValueError("count must be int")
|
|
||||||
if start > end:
|
|
||||||
raise ValueError("end must be equal to or greater than start.")
|
|
||||||
if count < 1:
|
|
||||||
raise ValueError("count must be equal to or greater than 1.")
|
|
||||||
if (end - start) / count < min_interval_sec:
|
|
||||||
count = int((end - start) / min_interval_sec)
|
|
||||||
if count == 0:
|
|
||||||
count = 1
|
|
||||||
interval = (end - start) / count
|
|
||||||
|
|
||||||
if count == 1:
|
|
||||||
return [start]
|
|
||||||
return sorted(list(set([int(start + interval * j)
|
|
||||||
for j in range(count)])))
|
|
||||||
|
|
||||||
|
|
||||||
def ready_blocks(video_id, duration, div, callback):
|
|
||||||
if div <= 0:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
async def _get_blocks(video_id, duration, div, callback):
|
|
||||||
async with httpx.AsyncClient(http2=True) as session:
|
|
||||||
tasks = [_create_block(session, video_id, seektime, callback)
|
|
||||||
for seektime in _split(-1, duration, div)]
|
|
||||||
return await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def _create_block(session, video_id, seektime, callback):
|
|
||||||
continuation = arcparam.getparam(video_id, seektime=seektime)
|
|
||||||
url = f"{REPLAY_URL}{quote(continuation)}&pbj=1"
|
|
||||||
for _ in range(MAX_RETRY_COUNT):
|
|
||||||
try:
|
|
||||||
resp = await session.get(url, headers=headers)
|
|
||||||
next_continuation, actions = parser.parse(resp.json())
|
|
||||||
break
|
|
||||||
except JSONDecodeError:
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
else:
|
|
||||||
cancel()
|
|
||||||
raise UnknownConnectionError("Abort: Unknown connection error.")
|
|
||||||
|
|
||||||
if actions:
|
|
||||||
first = parser.get_offset(actions[0])
|
|
||||||
last = parser.get_offset(actions[-1])
|
|
||||||
if callback:
|
|
||||||
callback(actions, last - first)
|
|
||||||
return Block(
|
|
||||||
continuation=next_continuation,
|
|
||||||
chat_data=actions,
|
|
||||||
first=first,
|
|
||||||
last=last
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
fetch initial blocks.
|
|
||||||
"""
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
blocks = loop.run_until_complete(
|
|
||||||
_get_blocks(video_id, duration, div, callback))
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_patch(callback, blocks, video_id):
|
|
||||||
|
|
||||||
async def _allocate_workers():
|
|
||||||
workers = [
|
|
||||||
ExtractWorker(
|
|
||||||
fetch=_fetch, block=block,
|
|
||||||
blocks=blocks, video_id=video_id
|
|
||||||
)
|
|
||||||
for block in blocks
|
|
||||||
]
|
|
||||||
async with httpx.AsyncClient() as session:
|
|
||||||
tasks = [worker.run(session) for worker in workers]
|
|
||||||
return await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def _fetch(continuation, session) -> Patch:
|
|
||||||
url = f"{REPLAY_URL}{quote(continuation)}&pbj=1"
|
|
||||||
for _ in range(MAX_RETRY_COUNT):
|
|
||||||
try:
|
|
||||||
resp = await session.get(url, headers=config.headers)
|
|
||||||
continuation, actions = parser.parse(resp.json())
|
|
||||||
break
|
|
||||||
except JSONDecodeError:
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
else:
|
|
||||||
cancel()
|
|
||||||
raise UnknownConnectionError("Abort: Unknown connection error.")
|
|
||||||
|
|
||||||
if actions:
|
|
||||||
last = parser.get_offset(actions[-1])
|
|
||||||
first = parser.get_offset(actions[0])
|
|
||||||
if callback:
|
|
||||||
callback(actions, last - first)
|
|
||||||
return Patch(actions, continuation, first, last)
|
|
||||||
return Patch(continuation=continuation)
|
|
||||||
|
|
||||||
"""
|
|
||||||
allocate workers and assign blocks.
|
|
||||||
"""
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(_allocate_workers())
|
|
||||||
except CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def _shutdown():
|
|
||||||
print("\nshutdown...")
|
|
||||||
tasks = [t for t in asyncio.all_tasks()
|
|
||||||
if t is not asyncio.current_task()]
|
|
||||||
for task in tasks:
|
|
||||||
task.cancel()
|
|
||||||
try:
|
|
||||||
await task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def cancel():
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.create_task(_shutdown())
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
class Block:
|
|
||||||
"""Block object represents something like a box
|
|
||||||
to join chunk of chatdata.
|
|
||||||
|
|
||||||
Parameter:
|
|
||||||
---------
|
|
||||||
first : int :
|
|
||||||
videoOffsetTimeMs of the first chat_data
|
|
||||||
(chat_data[0])
|
|
||||||
|
|
||||||
last : int :
|
|
||||||
videoOffsetTimeMs of the last chat_data.
|
|
||||||
(chat_data[-1])
|
|
||||||
|
|
||||||
this value increases as fetching chatdata progresses.
|
|
||||||
|
|
||||||
end : int :
|
|
||||||
target videoOffsetTimeMs of last chat data for extract,
|
|
||||||
equals to first videoOffsetTimeMs of next block.
|
|
||||||
when extract worker reaches this offset, stop fetching.
|
|
||||||
|
|
||||||
continuation : str :
|
|
||||||
continuation param of last chat data.
|
|
||||||
|
|
||||||
chat_data : list
|
|
||||||
|
|
||||||
done : bool :
|
|
||||||
whether this block has been fetched.
|
|
||||||
|
|
||||||
remaining : int :
|
|
||||||
remaining data to extract.
|
|
||||||
equals end - last.
|
|
||||||
|
|
||||||
is_last : bool :
|
|
||||||
whether this block is the last one in blocklist.
|
|
||||||
|
|
||||||
during_split : bool :
|
|
||||||
whether this block is in the process of during_split.
|
|
||||||
while True, this block is excluded from duplicate split procedure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ['first', 'last', 'end', 'continuation', 'chat_data', 'remaining',
|
|
||||||
'done', 'is_last', 'during_split']
|
|
||||||
|
|
||||||
def __init__(self, first=0, last=0, end=0,
|
|
||||||
continuation='', chat_data=[], is_last=False,
|
|
||||||
during_split=False):
|
|
||||||
self.first = first
|
|
||||||
self.last = last
|
|
||||||
self.end = end
|
|
||||||
self.continuation = continuation
|
|
||||||
self.chat_data = chat_data
|
|
||||||
self.done = False
|
|
||||||
self.remaining = self.end - self.last
|
|
||||||
self.is_last = is_last
|
|
||||||
self.during_split = during_split
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
from . import parser
|
|
||||||
|
|
||||||
|
|
||||||
def check_duplicate(chatdata):
|
|
||||||
max_range = len(chatdata) - 1
|
|
||||||
tbl_offset = [None] * max_range
|
|
||||||
tbl_id = [None] * max_range
|
|
||||||
tbl_type = [None] * max_range
|
|
||||||
|
|
||||||
def create_table(chatdata, max_range):
|
|
||||||
for i in range(max_range):
|
|
||||||
tbl_offset[i] = parser.get_offset(chatdata[i])
|
|
||||||
tbl_id[i] = parser.get_id(chatdata[i])
|
|
||||||
tbl_type[i] = parser.get_type(chatdata[i])
|
|
||||||
|
|
||||||
def is_duplicate(i, j):
|
|
||||||
return (
|
|
||||||
tbl_offset[i] == tbl_offset[j]
|
|
||||||
and tbl_id[i] == tbl_id[j]
|
|
||||||
and tbl_type[i] == tbl_type[j]
|
|
||||||
)
|
|
||||||
print("creating table...")
|
|
||||||
create_table(chatdata, max_range)
|
|
||||||
print("searching duplicate data...")
|
|
||||||
return [{"i": {
|
|
||||||
"index": i, "id": parser.get_id(chatdata[i]),
|
|
||||||
"offsetTime": parser.get_offset(chatdata[i]),
|
|
||||||
"type": parser.get_type(chatdata[i])
|
|
||||||
},
|
|
||||||
"j":{
|
|
||||||
"index": j, "id": parser.get_id(chatdata[j]),
|
|
||||||
"offsetTime": parser.get_offset(chatdata[j]),
|
|
||||||
"type": parser.get_type(chatdata[j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i in range(max_range) for j in range(i + 1, max_range)
|
|
||||||
if is_duplicate(i, j)]
|
|
||||||
|
|
||||||
|
|
||||||
def check_duplicate_offset(chatdata):
|
|
||||||
max_range = len(chatdata)
|
|
||||||
tbl_offset = [None] * max_range
|
|
||||||
tbl_id = [None] * max_range
|
|
||||||
tbl_type = [None] * max_range
|
|
||||||
|
|
||||||
def create_table(chatdata, max_range):
|
|
||||||
for i in range(max_range):
|
|
||||||
tbl_offset[i] = parser.get_offset(chatdata[i])
|
|
||||||
tbl_id[i] = parser.get_id(chatdata[i])
|
|
||||||
tbl_type[i] = parser.get_type(chatdata[i])
|
|
||||||
|
|
||||||
def is_duplicate(i, j):
|
|
||||||
return (
|
|
||||||
tbl_offset[i] == tbl_offset[j]
|
|
||||||
and tbl_id[i] == tbl_id[j]
|
|
||||||
)
|
|
||||||
|
|
||||||
print("creating table...")
|
|
||||||
create_table(chatdata, max_range)
|
|
||||||
print("searching duplicate data...")
|
|
||||||
|
|
||||||
return [{
|
|
||||||
"index": i, "id": tbl_id[i],
|
|
||||||
"offsetTime": tbl_offset[i],
|
|
||||||
"type:": tbl_type[i]
|
|
||||||
}
|
|
||||||
for i in range(max_range - 1)
|
|
||||||
if is_duplicate(i, i + 1)]
|
|
||||||
|
|
||||||
|
|
||||||
def remove_duplicate_head(blocks):
|
|
||||||
if len(blocks) == 0 or len(blocks) == 1:
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
def is_duplicate_head(index):
|
|
||||||
|
|
||||||
if len(blocks[index].chat_data) == 0:
|
|
||||||
return True
|
|
||||||
elif len(blocks[index + 1].chat_data) == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
id_0 = parser.get_id(blocks[index].chat_data[0])
|
|
||||||
id_1 = parser.get_id(blocks[index + 1].chat_data[0])
|
|
||||||
type_0 = parser.get_type(blocks[index].chat_data[0])
|
|
||||||
type_1 = parser.get_type(blocks[index + 1].chat_data[0])
|
|
||||||
return (
|
|
||||||
blocks[index].first == blocks[index + 1].first
|
|
||||||
and id_0 == id_1
|
|
||||||
and type_0 == type_1
|
|
||||||
)
|
|
||||||
ret = [blocks[i] for i in range(len(blocks) - 1)
|
|
||||||
if (len(blocks[i].chat_data) > 0
|
|
||||||
and not is_duplicate_head(i))]
|
|
||||||
ret.append(blocks[-1])
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def remove_duplicate_tail(blocks):
|
|
||||||
if len(blocks) == 0 or len(blocks) == 1:
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
def is_duplicate_tail(index):
|
|
||||||
if len(blocks[index].chat_data) == 0:
|
|
||||||
return True
|
|
||||||
elif len(blocks[index - 1].chat_data) == 0:
|
|
||||||
return False
|
|
||||||
id_0 = parser.get_id(blocks[index - 1].chat_data[-1])
|
|
||||||
id_1 = parser.get_id(blocks[index].chat_data[-1])
|
|
||||||
type_0 = parser.get_type(blocks[index - 1].chat_data[-1])
|
|
||||||
type_1 = parser.get_type(blocks[index].chat_data[-1])
|
|
||||||
return (
|
|
||||||
blocks[index - 1].last == blocks[index].last
|
|
||||||
and id_0 == id_1
|
|
||||||
and type_0 == type_1
|
|
||||||
)
|
|
||||||
|
|
||||||
ret = [blocks[i] for i in range(0, len(blocks))
|
|
||||||
if i == 0 or not is_duplicate_tail(i)]
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def remove_overlap(blocks):
|
|
||||||
"""
|
|
||||||
Fix overlapped blocks after ready_blocks().
|
|
||||||
Align the last offset of each block to the first offset
|
|
||||||
of next block (equals `end` offset of each block).
|
|
||||||
"""
|
|
||||||
if len(blocks) == 0 or len(blocks) == 1:
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
for block in blocks:
|
|
||||||
if block.is_last:
|
|
||||||
break
|
|
||||||
if len(block.chat_data) == 0:
|
|
||||||
continue
|
|
||||||
block_end = block.end
|
|
||||||
if block.last >= block_end:
|
|
||||||
for line in reversed(block.chat_data):
|
|
||||||
if parser.get_offset(line) < block_end:
|
|
||||||
break
|
|
||||||
block.chat_data.pop()
|
|
||||||
block.last = parser.get_offset(line)
|
|
||||||
block.remaining = 0
|
|
||||||
block.done = True
|
|
||||||
block.continuation = None
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
|
|
||||||
def _dump(blocks):
|
|
||||||
print("---------- first last end---")
|
|
||||||
for i, block in enumerate(blocks):
|
|
||||||
print(
|
|
||||||
f"block[{i:3}] {block.first:>10} {block.last:>10} {block.end:>10}")
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
from . import asyncdl
|
|
||||||
from . import duplcheck
|
|
||||||
from .. videoinfo import VideoInfo
|
|
||||||
from ... import config
|
|
||||||
from ... exceptions import InvalidVideoIdException
|
|
||||||
from ... util.extract_video_id import extract_video_id
|
|
||||||
|
|
||||||
logger = config.logger(__name__)
|
|
||||||
headers = config.headers
|
|
||||||
|
|
||||||
|
|
||||||
class Extractor:
|
|
||||||
def __init__(self, video_id, div=1, callback=None, processor=None):
|
|
||||||
if not isinstance(div, int) or div < 1:
|
|
||||||
raise ValueError('div must be positive integer.')
|
|
||||||
elif div > 10:
|
|
||||||
div = 10
|
|
||||||
self.video_id = extract_video_id(video_id)
|
|
||||||
self.div = div
|
|
||||||
self.callback = callback
|
|
||||||
self.processor = processor
|
|
||||||
self.duration = self._get_duration_of_video(video_id)
|
|
||||||
self.blocks = []
|
|
||||||
|
|
||||||
def _get_duration_of_video(self, video_id):
|
|
||||||
duration = 0
|
|
||||||
try:
|
|
||||||
duration = VideoInfo(video_id).get_duration()
|
|
||||||
except InvalidVideoIdException:
|
|
||||||
raise
|
|
||||||
return duration
|
|
||||||
|
|
||||||
def _ready_blocks(self):
|
|
||||||
blocks = asyncdl.ready_blocks(
|
|
||||||
self.video_id, self.duration, self.div, self.callback)
|
|
||||||
self.blocks = [block for block in blocks if block]
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _remove_duplicate_head(self):
|
|
||||||
self.blocks = duplcheck.remove_duplicate_head(self.blocks)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _set_block_end(self):
|
|
||||||
if len(self.blocks) > 0:
|
|
||||||
for i in range(len(self.blocks) - 1):
|
|
||||||
self.blocks[i].end = self.blocks[i + 1].first
|
|
||||||
self.blocks[-1].end = self.duration * 1000
|
|
||||||
self.blocks[-1].is_last = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _remove_overlap(self):
|
|
||||||
self.blocks = duplcheck.remove_overlap(self.blocks)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _download_blocks(self):
|
|
||||||
asyncdl.fetch_patch(self.callback, self.blocks, self.video_id)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _remove_duplicate_tail(self):
|
|
||||||
self.blocks = duplcheck.remove_duplicate_tail(self.blocks)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _combine(self):
|
|
||||||
ret = []
|
|
||||||
for block in self.blocks:
|
|
||||||
ret.extend(block.chat_data)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def _execute_extract_operations(self):
|
|
||||||
return (
|
|
||||||
self._ready_blocks()
|
|
||||||
._remove_duplicate_head()
|
|
||||||
._set_block_end()
|
|
||||||
._remove_overlap()
|
|
||||||
._download_blocks()
|
|
||||||
._remove_duplicate_tail()
|
|
||||||
._combine()
|
|
||||||
)
|
|
||||||
|
|
||||||
def extract(self):
|
|
||||||
if self.duration == 0:
|
|
||||||
print("\nCannot extract chat data:\n The specified video has not yet been archived.")
|
|
||||||
return []
|
|
||||||
data = self._execute_extract_operations()
|
|
||||||
if self.processor is None:
|
|
||||||
return data
|
|
||||||
ret = self.processor.process(
|
|
||||||
[{'video_id': None,
|
|
||||||
'timeout': 1,
|
|
||||||
'chatdata': (action["replayChatItemAction"]["actions"][0] for action in data)}]
|
|
||||||
)
|
|
||||||
self.processor.finalize()
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
asyncdl.cancel()
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
from ... import config
|
|
||||||
from ... import exceptions
|
|
||||||
|
|
||||||
logger = config.logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def parse(jsn):
|
|
||||||
"""
|
|
||||||
Parse replay chat data.
|
|
||||||
Parameter:
|
|
||||||
----------
|
|
||||||
jsn : dict
|
|
||||||
JSON of replay chat data.
|
|
||||||
Returns:
|
|
||||||
------
|
|
||||||
continuation : str
|
|
||||||
actions : list
|
|
||||||
|
|
||||||
"""
|
|
||||||
if jsn is None:
|
|
||||||
raise ValueError("parameter JSON is None")
|
|
||||||
if jsn['response']['responseContext'].get('errors'):
|
|
||||||
raise exceptions.ResponseContextError(
|
|
||||||
'video_id is invalid or private/deleted.')
|
|
||||||
contents = jsn['response'].get('continuationContents')
|
|
||||||
if contents is None:
|
|
||||||
raise exceptions.NoContents('No chat data.')
|
|
||||||
|
|
||||||
cont = contents['liveChatContinuation']['continuations'][0]
|
|
||||||
if cont is None:
|
|
||||||
raise exceptions.NoContinuation('No Continuation')
|
|
||||||
metadata = cont.get('liveChatReplayContinuationData')
|
|
||||||
if metadata:
|
|
||||||
continuation = metadata.get("continuation")
|
|
||||||
actions = contents['liveChatContinuation'].get('actions')
|
|
||||||
return continuation, actions
|
|
||||||
return None, []
|
|
||||||
|
|
||||||
|
|
||||||
def get_offset(item):
|
|
||||||
return int(item['replayChatItemAction']["videoOffsetTimeMsec"])
|
|
||||||
|
|
||||||
|
|
||||||
def get_id(item):
|
|
||||||
return list((list(item['replayChatItemAction']["actions"][0].values()
|
|
||||||
)[0])['item'].values())[0].get('id')
|
|
||||||
|
|
||||||
|
|
||||||
def get_type(item):
|
|
||||||
return list((list(item['replayChatItemAction']["actions"][0].values()
|
|
||||||
)[0])['item'].keys())[0]
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
from . import parser
|
|
||||||
from . block import Block
|
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
|
|
||||||
class Patch(NamedTuple):
|
|
||||||
"""
|
|
||||||
Patch represents chunk of chat data
|
|
||||||
which is fetched by asyncdl.fetch_patch._fetch().
|
|
||||||
"""
|
|
||||||
chats: list = []
|
|
||||||
continuation: str = None
|
|
||||||
first: int = None
|
|
||||||
last: int = None
|
|
||||||
|
|
||||||
|
|
||||||
def fill(block: Block, patch: Patch):
|
|
||||||
block_end = block.end
|
|
||||||
if patch.last < block_end or block.is_last:
|
|
||||||
set_patch(block, patch)
|
|
||||||
return
|
|
||||||
for line in reversed(patch.chats):
|
|
||||||
line_offset = parser.get_offset(line)
|
|
||||||
if line_offset < block_end:
|
|
||||||
break
|
|
||||||
patch.chats.pop()
|
|
||||||
set_patch(block, patch._replace(
|
|
||||||
continuation=None,
|
|
||||||
last=line_offset
|
|
||||||
)
|
|
||||||
)
|
|
||||||
block.remaining = 0
|
|
||||||
block.done = True
|
|
||||||
|
|
||||||
|
|
||||||
def split(parent_block: Block, child_block: Block, patch: Patch):
|
|
||||||
parent_block.during_split = False
|
|
||||||
if patch.first <= parent_block.last:
|
|
||||||
''' When patch overlaps with parent_block,
|
|
||||||
discard this block. '''
|
|
||||||
child_block.continuation = None
|
|
||||||
''' Leave child_block.during_split == True
|
|
||||||
to exclude from during_split sequence. '''
|
|
||||||
return
|
|
||||||
child_block.during_split = False
|
|
||||||
child_block.first = patch.first
|
|
||||||
parent_block.end = patch.first
|
|
||||||
fill(child_block, patch)
|
|
||||||
|
|
||||||
|
|
||||||
def set_patch(block: Block, patch: Patch):
|
|
||||||
block.continuation = patch.continuation
|
|
||||||
block.chat_data.extend(patch.chats)
|
|
||||||
block.last = patch.last
|
|
||||||
block.remaining = block.end - block.last
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
from . block import Block
|
|
||||||
from . patch import fill, split
|
|
||||||
from ... paramgen import arcparam
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
|
|
||||||
class ExtractWorker:
|
|
||||||
"""
|
|
||||||
ExtractWorker associates a download session with a block.
|
|
||||||
|
|
||||||
When the worker finishes fetching, the block
|
|
||||||
being fetched is splitted and assigned the free worker.
|
|
||||||
|
|
||||||
Parameter
|
|
||||||
----------
|
|
||||||
fetch : func :
|
|
||||||
extract function of asyncdl
|
|
||||||
|
|
||||||
block : Block :
|
|
||||||
Block object that includes chat_data
|
|
||||||
|
|
||||||
blocks : list :
|
|
||||||
List of Block(s)
|
|
||||||
|
|
||||||
video_id : str :
|
|
||||||
|
|
||||||
parent_block : Block :
|
|
||||||
the block from which current block is splitted
|
|
||||||
"""
|
|
||||||
__slots__ = ['block', 'fetch', 'blocks', 'video_id', 'parent_block']
|
|
||||||
|
|
||||||
def __init__(self, fetch, block, blocks, video_id):
|
|
||||||
self.block = block
|
|
||||||
self.fetch = fetch
|
|
||||||
self.blocks = blocks
|
|
||||||
self.video_id = video_id
|
|
||||||
self.parent_block = None
|
|
||||||
|
|
||||||
async def run(self, session):
|
|
||||||
while self.block.continuation:
|
|
||||||
patch = await self.fetch(
|
|
||||||
self.block.continuation, session)
|
|
||||||
if patch.continuation is None:
|
|
||||||
"""TODO : make the worker assigned to the last block
|
|
||||||
to work more than twice as possible.
|
|
||||||
"""
|
|
||||||
break
|
|
||||||
if self.parent_block:
|
|
||||||
split(self.parent_block, self.block, patch)
|
|
||||||
self.parent_block = None
|
|
||||||
else:
|
|
||||||
fill(self.block, patch)
|
|
||||||
if self.block.continuation is None:
|
|
||||||
"""finished fetching this block """
|
|
||||||
self.block.done = True
|
|
||||||
self.block = _search_new_block(self)
|
|
||||||
|
|
||||||
|
|
||||||
def _search_new_block(worker) -> Block:
|
|
||||||
index, undone_block = _get_undone_block(worker.blocks)
|
|
||||||
if undone_block is None:
|
|
||||||
return Block(continuation=None)
|
|
||||||
mean = (undone_block.last + undone_block.end) / 2
|
|
||||||
continuation = arcparam.getparam(worker.video_id, seektime=mean / 1000)
|
|
||||||
worker.parent_block = undone_block
|
|
||||||
worker.parent_block.during_split = True
|
|
||||||
new_block = Block(
|
|
||||||
end=undone_block.end,
|
|
||||||
chat_data=[],
|
|
||||||
continuation=continuation,
|
|
||||||
during_split=True,
|
|
||||||
is_last=worker.parent_block.is_last)
|
|
||||||
'''swap last block'''
|
|
||||||
if worker.parent_block.is_last:
|
|
||||||
worker.parent_block.is_last = False
|
|
||||||
worker.blocks.insert(index + 1, new_block)
|
|
||||||
return new_block
|
|
||||||
|
|
||||||
|
|
||||||
def _get_undone_block(blocks) -> Tuple[int, Block]:
|
|
||||||
min_interval_ms = 120000
|
|
||||||
max_remaining = 0
|
|
||||||
undone_block = None
|
|
||||||
index_undone_block = 0
|
|
||||||
for index, block in enumerate(blocks):
|
|
||||||
if block.done or block.during_split:
|
|
||||||
continue
|
|
||||||
remaining = block.remaining
|
|
||||||
if remaining > max_remaining and remaining > min_interval_ms:
|
|
||||||
index_undone_block = index
|
|
||||||
undone_block = block
|
|
||||||
max_remaining = remaining
|
|
||||||
return index_undone_block, undone_block
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
|
|
||||||
import httpx
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
from . import parser
|
|
||||||
from . block import Block
|
|
||||||
from . worker import ExtractWorker
|
|
||||||
from . patch import Patch
|
|
||||||
from ... import config
|
|
||||||
from ... paramgen import arcparam_mining as arcparam
|
|
||||||
from concurrent.futures import CancelledError
|
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
headers = config.headers
|
|
||||||
REPLAY_URL = "https://www.youtube.com/live_chat_replay?continuation="
|
|
||||||
INTERVAL = 1
|
|
||||||
|
|
||||||
|
|
||||||
def _split(start, end, count, min_interval_sec=120):
|
|
||||||
"""
|
|
||||||
Split section from `start` to `end` into `count` pieces,
|
|
||||||
and returns the beginning of each piece.
|
|
||||||
The `count` is adjusted so that the length of each piece
|
|
||||||
is no smaller than `min_interval`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
--------
|
|
||||||
List of the offset of each block's first chat data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not (isinstance(start, int) or isinstance(start, float)) or \
|
|
||||||
not (isinstance(end, int) or isinstance(end, float)):
|
|
||||||
raise ValueError("start/end must be int or float")
|
|
||||||
if not isinstance(count, int):
|
|
||||||
raise ValueError("count must be int")
|
|
||||||
if start > end:
|
|
||||||
raise ValueError("end must be equal to or greater than start.")
|
|
||||||
if count < 1:
|
|
||||||
raise ValueError("count must be equal to or greater than 1.")
|
|
||||||
if (end - start) / count < min_interval_sec:
|
|
||||||
count = int((end - start) / min_interval_sec)
|
|
||||||
if count == 0:
|
|
||||||
count = 1
|
|
||||||
interval = (end - start) / count
|
|
||||||
|
|
||||||
if count == 1:
|
|
||||||
return [start]
|
|
||||||
return sorted(list(set([int(start + interval * j)
|
|
||||||
for j in range(count)])))
|
|
||||||
|
|
||||||
|
|
||||||
def ready_blocks(video_id, duration, div, callback):
|
|
||||||
if div <= 0:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
async def _get_blocks(video_id, duration, div, callback):
|
|
||||||
async with httpx.ClientSession() as session:
|
|
||||||
tasks = [_create_block(session, video_id, seektime, callback)
|
|
||||||
for seektime in _split(0, duration, div)]
|
|
||||||
return await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def _create_block(session, video_id, seektime, callback):
|
|
||||||
continuation = arcparam.getparam(video_id, seektime=seektime)
|
|
||||||
url = (f"{REPLAY_URL}{quote(continuation)}&playerOffsetMs="
|
|
||||||
f"{int(seektime*1000)}&hidden=false&pbj=1")
|
|
||||||
async with session.get(url, headers=headers) as resp:
|
|
||||||
chat_json = await resp.text()
|
|
||||||
if chat_json is None:
|
|
||||||
return
|
|
||||||
continuation, actions = parser.parse(json.loads(chat_json)[1])
|
|
||||||
first = seektime
|
|
||||||
seektime += INTERVAL
|
|
||||||
if callback:
|
|
||||||
callback(actions, INTERVAL)
|
|
||||||
return Block(
|
|
||||||
continuation=continuation,
|
|
||||||
chat_data=actions,
|
|
||||||
first=first,
|
|
||||||
last=seektime,
|
|
||||||
seektime=seektime
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
fetch initial blocks.
|
|
||||||
"""
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
blocks = loop.run_until_complete(
|
|
||||||
_get_blocks(video_id, duration, div, callback))
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_patch(callback, blocks, video_id):
|
|
||||||
|
|
||||||
async def _allocate_workers():
|
|
||||||
workers = [
|
|
||||||
ExtractWorker(
|
|
||||||
fetch=_fetch, block=block,
|
|
||||||
blocks=blocks, video_id=video_id
|
|
||||||
)
|
|
||||||
for block in blocks
|
|
||||||
]
|
|
||||||
async with httpx.ClientSession() as session:
|
|
||||||
tasks = [worker.run(session) for worker in workers]
|
|
||||||
return await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def _fetch(seektime, session) -> Patch:
|
|
||||||
continuation = arcparam.getparam(video_id, seektime=seektime)
|
|
||||||
url = (f"{REPLAY_URL}{quote(continuation)}&playerOffsetMs="
|
|
||||||
f"{int(seektime*1000)}&hidden=false&pbj=1")
|
|
||||||
async with session.get(url, headers=config.headers) as resp:
|
|
||||||
chat_json = await resp.text()
|
|
||||||
actions = []
|
|
||||||
try:
|
|
||||||
if chat_json is None:
|
|
||||||
return Patch()
|
|
||||||
continuation, actions = parser.parse(json.loads(chat_json)[1])
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
if callback:
|
|
||||||
callback(actions, INTERVAL)
|
|
||||||
return Patch(chats=actions, continuation=continuation,
|
|
||||||
seektime=seektime, last=seektime)
|
|
||||||
"""
|
|
||||||
allocate workers and assign blocks.
|
|
||||||
"""
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(_allocate_workers())
|
|
||||||
except CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def _shutdown():
|
|
||||||
print("\nshutdown...")
|
|
||||||
tasks = [t for t in asyncio.all_tasks()
|
|
||||||
if t is not asyncio.current_task()]
|
|
||||||
for task in tasks:
|
|
||||||
task.cancel()
|
|
||||||
try:
|
|
||||||
await task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def cancel():
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.create_task(_shutdown())
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
from . import parser
|
|
||||||
class Block:
|
|
||||||
"""Block object represents something like a box
|
|
||||||
to join chunk of chatdata.
|
|
||||||
|
|
||||||
Parameter:
|
|
||||||
---------
|
|
||||||
first : int :
|
|
||||||
videoOffsetTimeMs of the first chat_data
|
|
||||||
(chat_data[0])
|
|
||||||
|
|
||||||
last : int :
|
|
||||||
videoOffsetTimeMs of the last chat_data.
|
|
||||||
(chat_data[-1])
|
|
||||||
|
|
||||||
this value increases as fetching chatdata progresses.
|
|
||||||
|
|
||||||
end : int :
|
|
||||||
target videoOffsetTimeMs of last chat data for extract,
|
|
||||||
equals to first videoOffsetTimeMs of next block.
|
|
||||||
when extract worker reaches this offset, stop fetching.
|
|
||||||
|
|
||||||
continuation : str :
|
|
||||||
continuation param of last chat data.
|
|
||||||
|
|
||||||
chat_data : list
|
|
||||||
|
|
||||||
done : bool :
|
|
||||||
whether this block has been fetched.
|
|
||||||
|
|
||||||
remaining : int :
|
|
||||||
remaining data to extract.
|
|
||||||
equals end - last.
|
|
||||||
|
|
||||||
is_last : bool :
|
|
||||||
whether this block is the last one in blocklist.
|
|
||||||
|
|
||||||
during_split : bool :
|
|
||||||
whether this block is in the process of during_split.
|
|
||||||
while True, this block is excluded from duplicate split procedure.
|
|
||||||
|
|
||||||
seektime : float :
|
|
||||||
the last position of this block(seconds) already fetched.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ['first','last','end','continuation','chat_data','remaining',
|
|
||||||
'done','is_last','during_split','seektime']
|
|
||||||
|
|
||||||
def __init__(self, first = 0, last = 0, end = 0,
|
|
||||||
continuation = '', chat_data = [], is_last = False,
|
|
||||||
during_split = False, seektime = None):
|
|
||||||
self.first = first
|
|
||||||
self.last = last
|
|
||||||
self.end = end
|
|
||||||
self.continuation = continuation
|
|
||||||
self.chat_data = chat_data
|
|
||||||
self.done = False
|
|
||||||
self.remaining = self.end - self.last
|
|
||||||
self.is_last = is_last
|
|
||||||
self.during_split = during_split
|
|
||||||
self.seektime = seektime
|
|
||||||
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import re
|
|
||||||
from ... import config
|
|
||||||
from ... exceptions import (
|
|
||||||
ResponseContextError,
|
|
||||||
NoContents, NoContinuation)
|
|
||||||
|
|
||||||
logger = config.logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def parse(jsn):
|
|
||||||
"""
|
|
||||||
Parse replay chat data.
|
|
||||||
Parameter:
|
|
||||||
----------
|
|
||||||
jsn : dict
|
|
||||||
JSON of replay chat data.
|
|
||||||
Returns:
|
|
||||||
------
|
|
||||||
continuation : str
|
|
||||||
actions : list
|
|
||||||
|
|
||||||
"""
|
|
||||||
if jsn is None:
|
|
||||||
raise ValueError("parameter JSON is None")
|
|
||||||
if jsn['response']['responseContext'].get('errors'):
|
|
||||||
raise ResponseContextError(
|
|
||||||
'video_id is invalid or private/deleted.')
|
|
||||||
contents = jsn["response"].get('continuationContents')
|
|
||||||
if contents is None:
|
|
||||||
raise NoContents('No chat data.')
|
|
||||||
|
|
||||||
cont = contents['liveChatContinuation']['continuations'][0]
|
|
||||||
if cont is None:
|
|
||||||
raise NoContinuation('No Continuation')
|
|
||||||
metadata = cont.get('liveChatReplayContinuationData')
|
|
||||||
if metadata:
|
|
||||||
continuation = metadata.get("continuation")
|
|
||||||
actions = contents['liveChatContinuation'].get('actions')
|
|
||||||
if continuation:
|
|
||||||
return continuation, [action["replayChatItemAction"]["actions"][0]
|
|
||||||
for action in actions
|
|
||||||
if list(action['replayChatItemAction']["actions"][0].values()
|
|
||||||
)[0]['item'].get("liveChatPaidMessageRenderer")
|
|
||||||
or list(action['replayChatItemAction']["actions"][0].values()
|
|
||||||
)[0]['item'].get("liveChatPaidStickerRenderer")
|
|
||||||
]
|
|
||||||
return None, []
|
|
||||||
|
|
||||||
|
|
||||||
def get_offset(item):
|
|
||||||
return int(item['replayChatItemAction']["videoOffsetTimeMsec"])
|
|
||||||
|
|
||||||
|
|
||||||
def get_id(item):
|
|
||||||
return list((list(item['replayChatItemAction']["actions"][0].values()
|
|
||||||
)[0])['item'].values())[0].get('id')
|
|
||||||
|
|
||||||
|
|
||||||
def get_type(item):
|
|
||||||
return list((list(item['replayChatItemAction']["actions"][0].values()
|
|
||||||
)[0])['item'].keys())[0]
|
|
||||||
|
|
||||||
|
|
||||||
_REGEX_YTINIT = re.compile(
|
|
||||||
"window\\[\"ytInitialData\"\\]\\s*=\\s*({.+?});\\s+")
|
|
||||||
|
|
||||||
|
|
||||||
def extract(text):
|
|
||||||
|
|
||||||
match = re.findall(_REGEX_YTINIT, str(text))
|
|
||||||
if match:
|
|
||||||
return match[0]
|
|
||||||
return None
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
from . import parser
|
|
||||||
from . block import Block
|
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
class Patch(NamedTuple):
|
|
||||||
"""
|
|
||||||
Patch represents chunk of chat data
|
|
||||||
which is fetched by asyncdl.fetch_patch._fetch().
|
|
||||||
"""
|
|
||||||
chats : list = []
|
|
||||||
continuation : str = None
|
|
||||||
seektime : float = None
|
|
||||||
first : int = None
|
|
||||||
last : int = None
|
|
||||||
|
|
||||||
def fill(block:Block, patch:Patch):
|
|
||||||
if patch.last < block.end:
|
|
||||||
set_patch(block, patch)
|
|
||||||
return
|
|
||||||
block.continuation = None
|
|
||||||
|
|
||||||
def set_patch(block:Block, patch:Patch):
|
|
||||||
block.continuation = patch.continuation
|
|
||||||
block.chat_data.extend(patch.chats)
|
|
||||||
block.last = patch.seektime
|
|
||||||
block.seektime = patch.seektime
|
|
||||||
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
from . import asyncdl
|
|
||||||
from . import parser
|
|
||||||
from .. videoinfo import VideoInfo
|
|
||||||
from ... import config
|
|
||||||
from ... exceptions import InvalidVideoIdException
|
|
||||||
logger = config.logger(__name__)
|
|
||||||
headers=config.headers
|
|
||||||
|
|
||||||
class SuperChatMiner:
|
|
||||||
def __init__(self, video_id, duration, div, callback):
|
|
||||||
if not isinstance(div ,int) or div < 1:
|
|
||||||
raise ValueError('div must be positive integer.')
|
|
||||||
elif div > 10:
|
|
||||||
div = 10
|
|
||||||
if not isinstance(duration ,int) or duration < 1:
|
|
||||||
raise ValueError('duration must be positive integer.')
|
|
||||||
self.video_id = video_id
|
|
||||||
self.duration = duration
|
|
||||||
self.div = div
|
|
||||||
self.callback = callback
|
|
||||||
self.blocks = []
|
|
||||||
|
|
||||||
def _ready_blocks(self):
|
|
||||||
blocks = asyncdl.ready_blocks(
|
|
||||||
self.video_id, self.duration, self.div, self.callback)
|
|
||||||
self.blocks = [block for block in blocks if block is not None]
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _set_block_end(self):
|
|
||||||
for i in range(len(self.blocks)-1):
|
|
||||||
self.blocks[i].end = self.blocks[i+1].first
|
|
||||||
self.blocks[-1].end = self.duration
|
|
||||||
self.blocks[-1].is_last =True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _download_blocks(self):
|
|
||||||
asyncdl.fetch_patch(self.callback, self.blocks, self.video_id)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _combine(self):
|
|
||||||
ret = []
|
|
||||||
for block in self.blocks:
|
|
||||||
ret.extend(block.chat_data)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def extract(self):
|
|
||||||
return (
|
|
||||||
self._ready_blocks()
|
|
||||||
._set_block_end()
|
|
||||||
._download_blocks()
|
|
||||||
._combine()
|
|
||||||
)
|
|
||||||
|
|
||||||
def extract(video_id, div = 1, callback = None, processor = None):
|
|
||||||
duration = 0
|
|
||||||
try:
|
|
||||||
duration = VideoInfo(video_id).get_duration()
|
|
||||||
except InvalidVideoIdException:
|
|
||||||
raise
|
|
||||||
if duration == 0:
|
|
||||||
print("video is live.")
|
|
||||||
return []
|
|
||||||
data = SuperChatMiner(video_id, duration, div, callback).extract()
|
|
||||||
if processor is None:
|
|
||||||
return data
|
|
||||||
return processor.process(
|
|
||||||
[{'video_id':None,'timeout':1,'chatdata' : (action
|
|
||||||
for action in data)}]
|
|
||||||
)
|
|
||||||
|
|
||||||
def cancel():
|
|
||||||
asyncdl.cancel()
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
from . import parser
|
|
||||||
from . block import Block
|
|
||||||
from . patch import Patch, fill
|
|
||||||
from ... paramgen import arcparam
|
|
||||||
INTERVAL = 1
|
|
||||||
class ExtractWorker:
|
|
||||||
"""
|
|
||||||
ExtractWorker associates a download session with a block.
|
|
||||||
|
|
||||||
When the worker finishes fetching, the block
|
|
||||||
being fetched is splitted and assigned the free worker.
|
|
||||||
|
|
||||||
Parameter
|
|
||||||
----------
|
|
||||||
fetch : func :
|
|
||||||
extract function of asyncdl
|
|
||||||
|
|
||||||
block : Block :
|
|
||||||
Block object that includes chat_data
|
|
||||||
|
|
||||||
blocks : list :
|
|
||||||
List of Block(s)
|
|
||||||
|
|
||||||
video_id : str :
|
|
||||||
|
|
||||||
parent_block : Block :
|
|
||||||
the block from which current block is splitted
|
|
||||||
"""
|
|
||||||
__slots__ = ['block', 'fetch', 'blocks', 'video_id', 'parent_block']
|
|
||||||
def __init__(self, fetch, block, blocks, video_id ):
|
|
||||||
self.block:Block = block
|
|
||||||
self.fetch = fetch
|
|
||||||
self.blocks:list = blocks
|
|
||||||
self.video_id:str = video_id
|
|
||||||
self.parent_block:Block = None
|
|
||||||
|
|
||||||
async def run(self, session):
|
|
||||||
while self.block.continuation:
|
|
||||||
patch = await self.fetch(
|
|
||||||
self.block.seektime, session)
|
|
||||||
fill(self.block, patch)
|
|
||||||
self.block.seektime += INTERVAL
|
|
||||||
self.block.done = True
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import json
|
|
||||||
import re
|
|
||||||
import httpx
|
|
||||||
from .. import config
|
|
||||||
from ..exceptions import InvalidVideoIdException, PatternUnmatchError
|
|
||||||
from ..util.extract_video_id import extract_video_id
|
|
||||||
|
|
||||||
headers = config.headers
|
|
||||||
|
|
||||||
pattern = re.compile(r"'PLAYER_CONFIG': ({.*}}})")
|
|
||||||
|
|
||||||
item_channel_id = [
|
|
||||||
"videoDetails",
|
|
||||||
"embeddedPlayerOverlayVideoDetailsRenderer",
|
|
||||||
"channelThumbnailEndpoint",
|
|
||||||
"channelThumbnailEndpoint",
|
|
||||||
"urlEndpoint",
|
|
||||||
"urlEndpoint",
|
|
||||||
"url"
|
|
||||||
]
|
|
||||||
|
|
||||||
item_renderer = [
|
|
||||||
"embedPreview",
|
|
||||||
"thumbnailPreviewRenderer"
|
|
||||||
]
|
|
||||||
|
|
||||||
item_response = [
|
|
||||||
"args",
|
|
||||||
"embedded_player_response"
|
|
||||||
]
|
|
||||||
|
|
||||||
item_author_image = [
|
|
||||||
"videoDetails",
|
|
||||||
"embeddedPlayerOverlayVideoDetailsRenderer",
|
|
||||||
"channelThumbnail",
|
|
||||||
"thumbnails",
|
|
||||||
0,
|
|
||||||
"url"
|
|
||||||
]
|
|
||||||
|
|
||||||
item_thumbnail = [
|
|
||||||
"defaultThumbnail",
|
|
||||||
"thumbnails",
|
|
||||||
2,
|
|
||||||
"url"
|
|
||||||
]
|
|
||||||
|
|
||||||
item_channel_name = [
|
|
||||||
"videoDetails",
|
|
||||||
"embeddedPlayerOverlayVideoDetailsRenderer",
|
|
||||||
"expandedRenderer",
|
|
||||||
"embeddedPlayerOverlayVideoDetailsExpandedRenderer",
|
|
||||||
"title",
|
|
||||||
"runs",
|
|
||||||
0,
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
|
|
||||||
item_moving_thumbnail = [
|
|
||||||
"movingThumbnail",
|
|
||||||
"thumbnails",
|
|
||||||
0,
|
|
||||||
"url"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class VideoInfo:
|
|
||||||
'''
|
|
||||||
VideoInfo object retrieves YouTube video information.
|
|
||||||
|
|
||||||
Parameter
|
|
||||||
---------
|
|
||||||
video_id : str
|
|
||||||
|
|
||||||
Exception
|
|
||||||
---------
|
|
||||||
InvalidVideoIdException :
|
|
||||||
Occurs when video_id does not exist on YouTube.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, video_id):
|
|
||||||
self.video_id = extract_video_id(video_id)
|
|
||||||
text = self._get_page_text(self.video_id)
|
|
||||||
self._parse(text)
|
|
||||||
|
|
||||||
def _get_page_text(self, video_id):
|
|
||||||
url = f"https://www.youtube.com/embed/{video_id}"
|
|
||||||
resp = httpx.get(url, headers=headers)
|
|
||||||
resp.raise_for_status()
|
|
||||||
return resp.text
|
|
||||||
|
|
||||||
def _parse(self, text):
|
|
||||||
result = re.search(pattern, text)
|
|
||||||
if result is None:
|
|
||||||
raise PatternUnmatchError(text)
|
|
||||||
decoder = json.JSONDecoder()
|
|
||||||
res = decoder.raw_decode(result.group(1)[:-1])[0]
|
|
||||||
response = self._get_item(res, item_response)
|
|
||||||
if response is None:
|
|
||||||
self._check_video_is_private(res.get("args"))
|
|
||||||
self._renderer = self._get_item(json.loads(response), item_renderer)
|
|
||||||
if self._renderer is None:
|
|
||||||
raise InvalidVideoIdException(
|
|
||||||
f"No renderer found in video_id: [{self.video_id}].")
|
|
||||||
|
|
||||||
def _check_video_is_private(self, args):
|
|
||||||
if args and args.get("video_id"):
|
|
||||||
raise InvalidVideoIdException(
|
|
||||||
f"video_id [{self.video_id}] is private or deleted.")
|
|
||||||
raise InvalidVideoIdException(
|
|
||||||
f"video_id [{self.video_id}] is invalid.")
|
|
||||||
|
|
||||||
def _get_item(self, dict_body, items: list):
|
|
||||||
for item in items:
|
|
||||||
if dict_body is None:
|
|
||||||
break
|
|
||||||
if isinstance(dict_body, dict):
|
|
||||||
dict_body = dict_body.get(item)
|
|
||||||
continue
|
|
||||||
if isinstance(item, int) and \
|
|
||||||
isinstance(dict_body, list) and \
|
|
||||||
len(dict_body) > item:
|
|
||||||
dict_body = dict_body[item]
|
|
||||||
continue
|
|
||||||
return None
|
|
||||||
return dict_body
|
|
||||||
|
|
||||||
def get_duration(self):
|
|
||||||
duration_seconds = self._renderer.get("videoDurationSeconds")
|
|
||||||
if duration_seconds:
|
|
||||||
'''Fetched value is string, so cast to integer.'''
|
|
||||||
return int(duration_seconds)
|
|
||||||
'''When key is not found, explicitly returns None.'''
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_title(self):
|
|
||||||
if self._renderer.get("title"):
|
|
||||||
return [''.join(run["text"])
|
|
||||||
for run in self._renderer["title"]["runs"]][0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_channel_id(self):
|
|
||||||
channel_url = self._get_item(self._renderer, item_channel_id)
|
|
||||||
if channel_url:
|
|
||||||
return channel_url[9:]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_author_image(self):
|
|
||||||
return self._get_item(self._renderer, item_author_image)
|
|
||||||
|
|
||||||
def get_thumbnail(self):
|
|
||||||
return self._get_item(self._renderer, item_thumbnail)
|
|
||||||
|
|
||||||
def get_channel_name(self):
|
|
||||||
return self._get_item(self._renderer, item_channel_name)
|
|
||||||
|
|
||||||
def get_moving_thumbnail(self):
|
|
||||||
return self._get_item(self._renderer, item_moving_thumbnail)
|
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
|
import datetime
|
||||||
import httpx
|
import httpx
|
||||||
import json
|
import json
|
||||||
import datetime
|
import os
|
||||||
|
import re
|
||||||
|
from urllib.parse import quote
|
||||||
from .. import config
|
from .. import config
|
||||||
|
from .. exceptions import InvalidVideoIdException
|
||||||
|
|
||||||
|
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
|
||||||
|
|
||||||
|
PATTERN_YTURL = re.compile(r"((?<=(v|V)/)|(?<=be/)|(?<=(\?|\&)v=)|(?<=embed/))([\w-]+)")
|
||||||
|
|
||||||
|
PATTERN_CHANNEL = re.compile(r"\\\"channelId\\\":\\\"(.{24})\\\"")
|
||||||
|
|
||||||
|
PATTERN_M_CHANNEL = re.compile(r"\"channelId\":\"(.{24})\"")
|
||||||
|
|
||||||
|
YT_VIDEO_ID_LENGTH = 11
|
||||||
|
|
||||||
|
CLIENT_VERSION = ''.join(("2.", (datetime.datetime.today() - datetime.timedelta(days=1)).strftime("%Y%m%d"), ".01.00"))
|
||||||
|
|
||||||
|
UA = config.headers["user-agent"]
|
||||||
|
|
||||||
|
|
||||||
def extract(url):
|
def extract(url):
|
||||||
@@ -12,7 +30,118 @@ def extract(url):
|
|||||||
json.dump(html.json(), f, ensure_ascii=False)
|
json.dump(html.json(), f, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
def save(data, filename, extention):
|
def save(data, filename, extention) -> str:
|
||||||
with open(filename + "_" + (datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')) + extention,
|
save_filename = filename + "_" + \
|
||||||
mode='w', encoding='utf-8') as f:
|
(datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')) + extention
|
||||||
|
with open(save_filename, mode='w', encoding='utf-8') as f:
|
||||||
f.writelines(data)
|
f.writelines(data)
|
||||||
|
return save_filename
|
||||||
|
|
||||||
|
|
||||||
|
def checkpath(filepath):
|
||||||
|
splitter = os.path.splitext(os.path.basename(filepath))
|
||||||
|
body = splitter[0]
|
||||||
|
extention = splitter[1]
|
||||||
|
newpath = filepath
|
||||||
|
counter = 1
|
||||||
|
while os.path.exists(newpath):
|
||||||
|
match = re.search(PATTERN, body)
|
||||||
|
if match:
|
||||||
|
counter = int(match[2]) + 1
|
||||||
|
num_with_bracket = f'({str(counter)})'
|
||||||
|
body = f'{match[1]}{num_with_bracket}'
|
||||||
|
else:
|
||||||
|
body = f'{body}({str(counter)})'
|
||||||
|
newpath = os.path.join(os.path.dirname(filepath), body + extention)
|
||||||
|
return newpath
|
||||||
|
|
||||||
|
|
||||||
|
def get_param(continuation, replay=False, offsetms: int = 0, dat=''):
|
||||||
|
if offsetms < 0:
|
||||||
|
offsetms = 0
|
||||||
|
ret = {
|
||||||
|
"context": {
|
||||||
|
"client": {
|
||||||
|
"visitorData": dat,
|
||||||
|
"userAgent": UA,
|
||||||
|
"clientName": "WEB",
|
||||||
|
"clientVersion": CLIENT_VERSION,
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
"continuation": continuation,
|
||||||
|
}
|
||||||
|
if replay:
|
||||||
|
ret.setdefault("currentPlayerState", {
|
||||||
|
"playerOffsetMs": str(int(offsetms))})
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def extract_video_id(url_or_id: str) -> str:
|
||||||
|
ret = ''
|
||||||
|
if '[' in url_or_id:
|
||||||
|
url_or_id = url_or_id.replace('[', '').replace(']', '')
|
||||||
|
|
||||||
|
if type(url_or_id) != str:
|
||||||
|
raise TypeError(f"{url_or_id}: URL or VideoID must be str, but {type(url_or_id)} is passed.")
|
||||||
|
if len(url_or_id) == YT_VIDEO_ID_LENGTH:
|
||||||
|
return url_or_id
|
||||||
|
match = re.search(PATTERN_YTURL, url_or_id)
|
||||||
|
if match is None:
|
||||||
|
raise InvalidVideoIdException(f"Invalid video id: {url_or_id}")
|
||||||
|
try:
|
||||||
|
ret = match.group(4)
|
||||||
|
except IndexError:
|
||||||
|
raise InvalidVideoIdException(f"Invalid video id: {url_or_id}")
|
||||||
|
|
||||||
|
if ret is None or len(ret) != YT_VIDEO_ID_LENGTH:
|
||||||
|
raise InvalidVideoIdException(f"Invalid video id: {url_or_id}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_channelid(client, video_id):
|
||||||
|
resp = client.get("https://www.youtube.com/embed/{}".format(quote(video_id)), headers=config.headers)
|
||||||
|
match = re.search(PATTERN_CHANNEL, resp.text)
|
||||||
|
try:
|
||||||
|
if match is None:
|
||||||
|
raise IndexError
|
||||||
|
ret = match.group(1)
|
||||||
|
except IndexError:
|
||||||
|
ret = get_channelid_2nd(client, video_id)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_channelid_2nd(client, video_id):
|
||||||
|
resp = client.get("https://m.youtube.com/watch?v={}".format(quote(video_id)), headers=config.m_headers)
|
||||||
|
|
||||||
|
match = re.search(PATTERN_M_CHANNEL, resp.text)
|
||||||
|
if match is None:
|
||||||
|
raise InvalidVideoIdException(f"Cannot find channel id for video id:{video_id}. This video id seems to be invalid.")
|
||||||
|
try:
|
||||||
|
ret = match.group(1)
|
||||||
|
except IndexError:
|
||||||
|
raise InvalidVideoIdException(f"Invalid video id: {video_id}")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
async def get_channelid_async(client, video_id):
|
||||||
|
resp = await client.get("https://www.youtube.com/embed/{}".format(quote(video_id)), headers=config.headers)
|
||||||
|
match = re.search(PATTERN_CHANNEL, resp.text)
|
||||||
|
try:
|
||||||
|
if match is None:
|
||||||
|
raise IndexError
|
||||||
|
ret = match.group(1)
|
||||||
|
except IndexError:
|
||||||
|
ret = await get_channelid_async_2nd(client, video_id)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def get_channelid_async_2nd(client, video_id):
|
||||||
|
resp = await client.get("https://m.youtube.com/watch?v={}".format(quote(video_id)), headers=config.m_headers)
|
||||||
|
match = re.search(PATTERN_M_CHANNEL, resp.text)
|
||||||
|
if match is None:
|
||||||
|
raise InvalidVideoIdException(f"Cannot find channel id for video id:{video_id}. This video id seems to be invalid.")
|
||||||
|
try:
|
||||||
|
ret = match.group(1)
|
||||||
|
except IndexError:
|
||||||
|
raise InvalidVideoIdException(f"Invalid video id: {video_id}")
|
||||||
|
return ret
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import re
|
|
||||||
from .. exceptions import InvalidVideoIdException
|
|
||||||
|
|
||||||
|
|
||||||
PATTERN = re.compile(r"((?<=(v|V)/)|(?<=be/)|(?<=(\?|\&)v=)|(?<=embed/))([\w-]+)")
|
|
||||||
YT_VIDEO_ID_LENGTH = 11
|
|
||||||
|
|
||||||
|
|
||||||
def extract_video_id(url_or_id: str) -> str:
|
|
||||||
ret = ''
|
|
||||||
if type(url_or_id) != str:
|
|
||||||
raise TypeError(f"{url_or_id}: URL or VideoID must be str, but {type(url_or_id)} is passed.")
|
|
||||||
if len(url_or_id) == YT_VIDEO_ID_LENGTH:
|
|
||||||
return url_or_id
|
|
||||||
match = re.search(PATTERN, url_or_id)
|
|
||||||
if match is None:
|
|
||||||
raise InvalidVideoIdException(url_or_id)
|
|
||||||
try:
|
|
||||||
ret = match.group(4)
|
|
||||||
except IndexError:
|
|
||||||
raise InvalidVideoIdException(url_or_id)
|
|
||||||
|
|
||||||
if ret is None or len(ret) != YT_VIDEO_ID_LENGTH:
|
|
||||||
raise InvalidVideoIdException(url_or_id)
|
|
||||||
return ret
|
|
||||||
@@ -1,4 +1 @@
|
|||||||
httpx[http2]==0.14.1
|
httpx[http2]
|
||||||
protobuf==3.13.0
|
|
||||||
pytz
|
|
||||||
urllib3
|
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
mock
|
pytest-mock
|
||||||
mocker
|
pytest-httpx
|
||||||
pytest
|
|
||||||
pytest_httpx
|
|
||||||
|
|||||||
10
setup.py
10
setup.py
@@ -50,16 +50,12 @@ setup(
|
|||||||
'Natural Language :: Japanese',
|
'Natural Language :: Japanese',
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.7',
|
||||||
'Programming Language :: Python :: 3.8',
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
],
|
],
|
||||||
description="a python library for fetching youtube live chat.",
|
description="a python library for fetching youtube live chat.",
|
||||||
entry_points=
|
|
||||||
'''
|
|
||||||
[console_scripts]
|
|
||||||
pytchat=pytchat.cli:main
|
|
||||||
''',
|
|
||||||
install_requires=_requirements(),
|
install_requires=_requirements(),
|
||||||
keywords='youtube livechat asyncio',
|
keywords='youtube livechat asyncio',
|
||||||
license=license,
|
license=license,
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import json
|
|
||||||
import httpx
|
|
||||||
import pytchat.config as config
|
|
||||||
from pytchat.paramgen import arcparam
|
|
||||||
from pytchat.parser.live import Parser
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_0(mocker):
|
|
||||||
param = arcparam.getparam("01234567890", -1)
|
|
||||||
assert param == "op2w0wQmGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09SARgAXICCAE%3D"
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_1(mocker):
|
|
||||||
param = arcparam.getparam("01234567890", seektime=100000)
|
|
||||||
assert param == "op2w0wQtGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09KIDQ28P0AkgDYAFyAggB"
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_2(mocker):
|
|
||||||
param = arcparam.getparam("SsjCnHOk-Sk", seektime=100)
|
|
||||||
url = f"https://www.youtube.com/live_chat_replay/get_live_chat_replay?continuation={param}&pbj=1"
|
|
||||||
resp = httpx.Client(http2=True).get(url, headers=config.headers)
|
|
||||||
jsn = json.loads(resp.text)
|
|
||||||
parser = Parser(is_replay=True)
|
|
||||||
contents = parser.get_contents(jsn)
|
|
||||||
_, chatdata = parser.parse(contents)
|
|
||||||
test_id = chatdata[0]["addChatItemAction"]["item"]["liveChatTextMessageRenderer"]["id"]
|
|
||||||
assert test_id == "CjoKGkNMYXBzZTdudHVVQ0Zjc0IxZ0FkTnFnQjVREhxDSnlBNHV2bnR1VUNGV0dnd2dvZDd3NE5aZy0w"
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_3(mocker):
|
|
||||||
param = arcparam.getparam("01234567890")
|
|
||||||
assert param == "op2w0wQmGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09SARgAXICCAE%3D"
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from pytchat.tool.mining import parser
|
|
||||||
import pytchat.config as config
|
|
||||||
import httpx
|
|
||||||
import json
|
|
||||||
from pytchat.paramgen import arcparam_mining as arcparam
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_e(mocker):
|
|
||||||
try:
|
|
||||||
arcparam.getparam("01234567890", -1)
|
|
||||||
assert False
|
|
||||||
except ValueError:
|
|
||||||
assert True
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_0(mocker):
|
|
||||||
param = arcparam.getparam("01234567890", 0)
|
|
||||||
|
|
||||||
assert param == "op2w0wQsGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABYARyAggBeAE%3D"
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_1(mocker):
|
|
||||||
param = arcparam.getparam("01234567890", seektime=100000)
|
|
||||||
print(param)
|
|
||||||
assert param == "op2w0wQzGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABWgUQgMLXL2AEcgIIAXgB"
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_2(mocker):
|
|
||||||
param = arcparam.getparam("PZz9NB0-Z64", 1)
|
|
||||||
url = f"https://www.youtube.com/live_chat_replay?continuation={param}&playerOffsetMs=1000&pbj=1"
|
|
||||||
resp = httpx.Client(http2=True).get(url, headers=config.headers)
|
|
||||||
jsn = json.loads(resp.text)
|
|
||||||
_, chatdata = parser.parse(jsn[1])
|
|
||||||
test_id = chatdata[0]["addChatItemAction"]["item"]["liveChatPaidMessageRenderer"]["id"]
|
|
||||||
print(test_id)
|
|
||||||
assert test_id == "ChwKGkNKSGE0YnFJeWVBQ0ZWcUF3Z0VkdGIwRm9R"
|
|
||||||
|
|
||||||
|
|
||||||
def test_arcparam_3(mocker):
|
|
||||||
param = arcparam.getparam("01234567890")
|
|
||||||
assert param == "op2w0wQsGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABYARyAggBeAE%3D"
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
from pytchat.processors.superchat.calculator import SuperchatCalculator
|
|
||||||
|
|
||||||
get_item = SuperchatCalculator()._get_item
|
|
||||||
|
|
||||||
dict_test = {
|
|
||||||
'root':{
|
|
||||||
'node0' : 'value0',
|
|
||||||
'node1' : 'value1',
|
|
||||||
'node2' : {
|
|
||||||
'node2-0' : 'value2-0'
|
|
||||||
},
|
|
||||||
|
|
||||||
'node3' : [
|
|
||||||
{'node3-0' : 'value3-0'},
|
|
||||||
{'node3-1' :
|
|
||||||
{'node3-1-0' : 'value3-1-0'}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'node4' : [],
|
|
||||||
'node5' : [
|
|
||||||
[
|
|
||||||
{'node5-1-0' : 'value5-1-0'},
|
|
||||||
{'node5-1-1' : 'value5-1-1'},
|
|
||||||
],
|
|
||||||
{'node5-0' : 'value5-0'},
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items_test0 = [
|
|
||||||
'root',
|
|
||||||
'node1'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
items_test_not_found0 = [
|
|
||||||
'root',
|
|
||||||
'other_data'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
items_test_nest = [
|
|
||||||
'root',
|
|
||||||
'node2',
|
|
||||||
'node2-0'
|
|
||||||
]
|
|
||||||
|
|
||||||
items_test_list0 = [
|
|
||||||
'root',
|
|
||||||
'node3',
|
|
||||||
1,
|
|
||||||
'node3-1'
|
|
||||||
]
|
|
||||||
|
|
||||||
items_test_list1 = [
|
|
||||||
'root',
|
|
||||||
'node3',
|
|
||||||
1,
|
|
||||||
'node3-1',
|
|
||||||
'node3-1-0'
|
|
||||||
]
|
|
||||||
|
|
||||||
items_test_list2 = [
|
|
||||||
'root',
|
|
||||||
'node4',
|
|
||||||
None
|
|
||||||
]
|
|
||||||
|
|
||||||
items_test_list3 = [
|
|
||||||
'root',
|
|
||||||
'node4'
|
|
||||||
]
|
|
||||||
|
|
||||||
items_test_list_nest = [
|
|
||||||
'root',
|
|
||||||
'node5',
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
'node5-1-1'
|
|
||||||
]
|
|
||||||
|
|
||||||
items_test_list_nest_not_found1 = [
|
|
||||||
'root',
|
|
||||||
'node5',
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
'node5-1-1',
|
|
||||||
'nodez'
|
|
||||||
]
|
|
||||||
|
|
||||||
items_test_not_found1 = [
|
|
||||||
'root',
|
|
||||||
'node3',
|
|
||||||
2,
|
|
||||||
'node3-1',
|
|
||||||
'node3-1-0'
|
|
||||||
]
|
|
||||||
|
|
||||||
items_test_not_found2 = [
|
|
||||||
'root',
|
|
||||||
'node3',
|
|
||||||
2,
|
|
||||||
'node3-1',
|
|
||||||
'node3-1-0',
|
|
||||||
'nodex'
|
|
||||||
]
|
|
||||||
def test_get_items_0():
|
|
||||||
assert get_item(dict_test, items_test0) == 'value1'
|
|
||||||
|
|
||||||
def test_get_items_1():
|
|
||||||
assert get_item(dict_test, items_test_not_found0) is None
|
|
||||||
|
|
||||||
def test_get_items_2():
|
|
||||||
assert get_item(dict_test, items_test_nest) == 'value2-0'
|
|
||||||
|
|
||||||
def test_get_items_3():
|
|
||||||
assert get_item(
|
|
||||||
dict_test, items_test_list0) == {'node3-1-0' : 'value3-1-0'}
|
|
||||||
|
|
||||||
def test_get_items_4():
|
|
||||||
assert get_item(dict_test, items_test_list1) == 'value3-1-0'
|
|
||||||
|
|
||||||
def test_get_items_5():
|
|
||||||
assert get_item(dict_test, items_test_not_found1) == None
|
|
||||||
|
|
||||||
def test_get_items_6():
|
|
||||||
assert get_item(dict_test, items_test_not_found2) == None
|
|
||||||
|
|
||||||
def test_get_items_7():
|
|
||||||
assert get_item(dict_test, items_test_list2) == None
|
|
||||||
|
|
||||||
def test_get_items_8():
|
|
||||||
assert get_item(dict_test, items_test_list_nest) == 'value5-1-1'
|
|
||||||
|
|
||||||
def test_get_items_9():
|
|
||||||
assert get_item(dict_test, items_test_list_nest_not_found1) == None
|
|
||||||
|
|
||||||
def test_get_items_10():
|
|
||||||
assert get_item(dict_test, items_test_list3) == []
|
|
||||||
@@ -6,50 +6,55 @@ parse = SuperchatCalculator()._parse
|
|||||||
|
|
||||||
|
|
||||||
def _open_file(path):
|
def _open_file(path):
|
||||||
with open(path,mode ='r',encoding = 'utf-8') as f:
|
with open(path, mode='r', encoding='utf-8') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
def load_chatdata(filepath):
|
def load_chatdata(filepath):
|
||||||
parser = Parser(is_replay=True)
|
parser = Parser(is_replay=True)
|
||||||
#print(json.loads(_open_file(filepath)))
|
# print(json.loads(_open_file(filepath)))
|
||||||
contents = parser.get_contents( json.loads(_open_file(filepath)))
|
contents = parser.get_contents(json.loads(_open_file(filepath)))[0]
|
||||||
return parser.parse(contents)[1]
|
return parser.parse(contents)[1]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_1():
|
def test_parse_1():
|
||||||
renderer ={"purchaseAmountText":{"simpleText":"¥2,000"}}
|
renderer = {"purchaseAmountText": {"simpleText": "¥2,000"}}
|
||||||
symbol ,amount = parse(renderer)
|
symbol, amount = parse(renderer)
|
||||||
assert symbol == '¥'
|
assert symbol == '¥'
|
||||||
assert amount == 2000.0
|
assert amount == 2000.0
|
||||||
|
|
||||||
|
|
||||||
def test_parse_2():
|
def test_parse_2():
|
||||||
renderer ={"purchaseAmountText":{"simpleText":"ABC\x0a200"}}
|
renderer = {"purchaseAmountText": {"simpleText": "ABC\x0a200"}}
|
||||||
symbol ,amount = parse(renderer)
|
symbol, amount = parse(renderer)
|
||||||
assert symbol == 'ABC\x0a'
|
assert symbol == 'ABC\x0a'
|
||||||
assert amount == 200.0
|
assert amount == 200.0
|
||||||
|
|
||||||
|
|
||||||
def test_process_0():
|
def test_process_0():
|
||||||
"""
|
"""
|
||||||
parse superchat data
|
parse superchat data
|
||||||
"""
|
"""
|
||||||
chat_component = {
|
chat_component = {
|
||||||
'video_id':'',
|
'video_id': '',
|
||||||
'timeout':10,
|
'timeout': 10,
|
||||||
'chatdata':load_chatdata(r"tests/testdata/calculator/superchat_0.json")
|
'chatdata': load_chatdata(r"tests/testdata/calculator/superchat_0.json")
|
||||||
}
|
}
|
||||||
assert SuperchatCalculator().process([chat_component])=={'¥': 6800.0, '€': 2.0}
|
assert SuperchatCalculator().process([chat_component]) == {
|
||||||
|
'¥': 6800.0, '€': 2.0}
|
||||||
|
|
||||||
|
|
||||||
def test_process_1():
|
def test_process_1():
|
||||||
"""
|
"""
|
||||||
parse no superchat data
|
parse no superchat data
|
||||||
"""
|
"""
|
||||||
chat_component = {
|
chat_component = {
|
||||||
'video_id':'',
|
'video_id': '',
|
||||||
'timeout':10,
|
'timeout': 10,
|
||||||
'chatdata':load_chatdata(r"tests/testdata/calculator/text_only.json")
|
'chatdata': load_chatdata(r"tests/testdata/calculator/text_only.json")
|
||||||
}
|
}
|
||||||
assert SuperchatCalculator().process([chat_component])=={}
|
assert SuperchatCalculator().process([chat_component]) == {}
|
||||||
|
|
||||||
|
|
||||||
def test_process_2():
|
def test_process_2():
|
||||||
"""
|
"""
|
||||||
@@ -57,12 +62,10 @@ def test_process_2():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
chat_component = {
|
chat_component = {
|
||||||
'video_id':'',
|
'video_id': '',
|
||||||
'timeout':10,
|
'timeout': 10,
|
||||||
'chatdata':load_chatdata(r"tests/testdata/calculator/replay_end.json")
|
'chatdata': load_chatdata(r"tests/testdata/calculator/replay_end.json")
|
||||||
}
|
}
|
||||||
assert False
|
assert False
|
||||||
SuperchatCalculator().process([chat_component])
|
|
||||||
except ChatParseException:
|
except ChatParseException:
|
||||||
assert True
|
assert True
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def test_textmessage(mocker):
|
|||||||
|
|
||||||
_json = _open_file("tests/testdata/compatible/textmessage.json")
|
_json = _open_file("tests/testdata/compatible/textmessage.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -51,7 +51,7 @@ def test_newsponcer(mocker):
|
|||||||
|
|
||||||
_json = _open_file("tests/testdata/compatible/newSponsor.json")
|
_json = _open_file("tests/testdata/compatible/newSponsor.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -88,7 +88,7 @@ def test_newsponcer_rev(mocker):
|
|||||||
|
|
||||||
_json = _open_file("tests/testdata/compatible/newSponsor_rev.json")
|
_json = _open_file("tests/testdata/compatible/newSponsor_rev.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -125,7 +125,7 @@ def test_superchat(mocker):
|
|||||||
|
|
||||||
_json = _open_file("tests/testdata/compatible/superchat.json")
|
_json = _open_file("tests/testdata/compatible/superchat.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -164,7 +164,7 @@ def test_unregistered_currency(mocker):
|
|||||||
|
|
||||||
_json = _open_file("tests/testdata/unregistered_currency.json")
|
_json = _open_file("tests/testdata/unregistered_currency.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
|
|||||||
24
tests/test_compatible_processor2.py
Normal file
24
tests/test_compatible_processor2.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import pytchat
|
||||||
|
from pytchat.processors.compatible.processor import CompatibleProcessor
|
||||||
|
|
||||||
|
|
||||||
|
root_keys = ('kind', 'etag', 'nextPageToken', 'pollingIntervalMillis', 'pageInfo', 'items')
|
||||||
|
item_keys = ('kind', 'etag', 'id', 'snippet', 'authorDetails')
|
||||||
|
snippet_keys = ('type', 'liveChatId', 'authorChannelId', 'publishedAt', 'hasDisplayContent', 'displayMessage', 'textMessageDetails')
|
||||||
|
author_details_keys = ('channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor', 'isChatModerator')
|
||||||
|
|
||||||
|
def test_compatible_processor():
|
||||||
|
stream = pytchat.create("Hj-wnLIYKjw", seektime = 6000, processor=CompatibleProcessor())
|
||||||
|
while stream.is_alive():
|
||||||
|
chat = stream.get()
|
||||||
|
for key in chat.keys():
|
||||||
|
assert key in root_keys
|
||||||
|
for key in chat["items"][0].keys():
|
||||||
|
assert key in item_keys
|
||||||
|
for key in chat["items"][0]["snippet"].keys():
|
||||||
|
assert key in snippet_keys
|
||||||
|
for key in chat["items"][0]["authorDetails"].keys():
|
||||||
|
assert key in author_details_keys
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from pytchat.parser.live import Parser
|
from pytchat.parser.live import Parser
|
||||||
from pytchat.processors.default.processor import DefaultProcessor
|
from pytchat.processors.default.processor import DefaultProcessor
|
||||||
|
|
||||||
|
|
||||||
|
TEST_TIMETSTAMP = 1570678496000000
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_datetime(timestamp):
|
||||||
|
dt = datetime.fromtimestamp(timestamp / 1000000)
|
||||||
|
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
|
||||||
def test_textmessage(mocker):
|
def test_textmessage(mocker):
|
||||||
'''text message'''
|
'''text message'''
|
||||||
processor = DefaultProcessor()
|
processor = DefaultProcessor()
|
||||||
parser = Parser(is_replay=False)
|
parser = Parser(is_replay=False)
|
||||||
_json = _open_file("tests/testdata/default/textmessage.json")
|
_json = _open_file("tests/testdata/default/textmessage.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -17,11 +26,10 @@ def test_textmessage(mocker):
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret = processor.process([data]).items[0]
|
ret = processor.process([data]).items[0]
|
||||||
assert ret.chattype == "textMessage"
|
|
||||||
assert ret.id == "dummy_id"
|
assert ret.id == "dummy_id"
|
||||||
assert ret.message == "dummy_message"
|
assert ret.message == "dummy_message"
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.author.name == "author_name"
|
assert ret.author.name == "author_name"
|
||||||
assert ret.author.channelId == "author_channel_id"
|
assert ret.author.channelId == "author_channel_id"
|
||||||
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
|
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
|
||||||
@@ -39,7 +47,7 @@ def test_textmessage_replay_member(mocker):
|
|||||||
parser = Parser(is_replay=True)
|
parser = Parser(is_replay=True)
|
||||||
_json = _open_file("tests/testdata/default/replay_member_text.json")
|
_json = _open_file("tests/testdata/default/replay_member_text.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -47,13 +55,12 @@ def test_textmessage_replay_member(mocker):
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret = processor.process([data]).items[0]
|
ret = processor.process([data]).items[0]
|
||||||
assert ret.chattype == "textMessage"
|
|
||||||
assert ret.type == "textMessage"
|
assert ret.type == "textMessage"
|
||||||
assert ret.id == "dummy_id"
|
assert ret.id == "dummy_id"
|
||||||
assert ret.message == "dummy_message"
|
assert ret.message == "dummy_message"
|
||||||
assert ret.messageEx == ["dummy_message"]
|
assert ret.messageEx == ["dummy_message"]
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == "1:23:45"
|
assert ret.elapsedTime == "1:23:45"
|
||||||
assert ret.author.name == "author_name"
|
assert ret.author.name == "author_name"
|
||||||
assert ret.author.channelId == "author_channel_id"
|
assert ret.author.channelId == "author_channel_id"
|
||||||
@@ -72,7 +79,7 @@ def test_superchat(mocker):
|
|||||||
parser = Parser(is_replay=False)
|
parser = Parser(is_replay=False)
|
||||||
_json = _open_file("tests/testdata/default/superchat.json")
|
_json = _open_file("tests/testdata/default/superchat.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -80,14 +87,12 @@ def test_superchat(mocker):
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret = processor.process([data]).items[0]
|
ret = processor.process([data]).items[0]
|
||||||
print(json.dumps(chatdata, ensure_ascii=False))
|
|
||||||
assert ret.chattype == "superChat"
|
|
||||||
assert ret.type == "superChat"
|
assert ret.type == "superChat"
|
||||||
assert ret.id == "dummy_id"
|
assert ret.id == "dummy_id"
|
||||||
assert ret.message == "dummy_message"
|
assert ret.message == "dummy_message"
|
||||||
assert ret.messageEx == ["dummy_message"]
|
assert ret.messageEx == ["dummy_message"]
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == ""
|
assert ret.elapsedTime == ""
|
||||||
assert ret.amountValue == 800
|
assert ret.amountValue == 800
|
||||||
assert ret.amountString == "¥800"
|
assert ret.amountString == "¥800"
|
||||||
@@ -116,7 +121,7 @@ def test_supersticker(mocker):
|
|||||||
parser = Parser(is_replay=False)
|
parser = Parser(is_replay=False)
|
||||||
_json = _open_file("tests/testdata/default/supersticker.json")
|
_json = _open_file("tests/testdata/default/supersticker.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -124,14 +129,12 @@ def test_supersticker(mocker):
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret = processor.process([data]).items[0]
|
ret = processor.process([data]).items[0]
|
||||||
print(json.dumps(chatdata, ensure_ascii=False))
|
|
||||||
assert ret.chattype == "superSticker"
|
|
||||||
assert ret.type == "superSticker"
|
assert ret.type == "superSticker"
|
||||||
assert ret.id == "dummy_id"
|
assert ret.id == "dummy_id"
|
||||||
assert ret.message == ""
|
assert ret.message == ""
|
||||||
assert ret.messageEx == []
|
assert ret.messageEx == []
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == ""
|
assert ret.elapsedTime == ""
|
||||||
assert ret.amountValue == 200
|
assert ret.amountValue == 200
|
||||||
assert ret.amountString == "¥200"
|
assert ret.amountString == "¥200"
|
||||||
@@ -159,7 +162,7 @@ def test_sponsor(mocker):
|
|||||||
parser = Parser(is_replay=False)
|
parser = Parser(is_replay=False)
|
||||||
_json = _open_file("tests/testdata/default/newSponsor_current.json")
|
_json = _open_file("tests/testdata/default/newSponsor_current.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -167,14 +170,12 @@ def test_sponsor(mocker):
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret = processor.process([data]).items[0]
|
ret = processor.process([data]).items[0]
|
||||||
print(json.dumps(chatdata, ensure_ascii=False))
|
|
||||||
assert ret.chattype == "newSponsor"
|
|
||||||
assert ret.type == "newSponsor"
|
assert ret.type == "newSponsor"
|
||||||
assert ret.id == "dummy_id"
|
assert ret.id == "dummy_id"
|
||||||
assert ret.message == "新規メンバー"
|
assert ret.message == "新規メンバー"
|
||||||
assert ret.messageEx == ["新規メンバー"]
|
assert ret.messageEx == ["新規メンバー"]
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == ""
|
assert ret.elapsedTime == ""
|
||||||
assert ret.bgColor == 0
|
assert ret.bgColor == 0
|
||||||
assert ret.author.name == "author_name"
|
assert ret.author.name == "author_name"
|
||||||
@@ -194,7 +195,7 @@ def test_sponsor_legacy(mocker):
|
|||||||
parser = Parser(is_replay=False)
|
parser = Parser(is_replay=False)
|
||||||
_json = _open_file("tests/testdata/default/newSponsor_lagacy.json")
|
_json = _open_file("tests/testdata/default/newSponsor_lagacy.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 7,
|
"timeout": 7,
|
||||||
@@ -202,14 +203,12 @@ def test_sponsor_legacy(mocker):
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret = processor.process([data]).items[0]
|
ret = processor.process([data]).items[0]
|
||||||
print(json.dumps(chatdata, ensure_ascii=False))
|
|
||||||
assert ret.chattype == "newSponsor"
|
|
||||||
assert ret.type == "newSponsor"
|
assert ret.type == "newSponsor"
|
||||||
assert ret.id == "dummy_id"
|
assert ret.id == "dummy_id"
|
||||||
assert ret.message == "新規メンバー / ようこそ、author_name!"
|
assert ret.message == "新規メンバー / ようこそ、author_name!"
|
||||||
assert ret.messageEx == ["新規メンバー / ようこそ、author_name!"]
|
assert ret.messageEx == ["新規メンバー / ようこそ、author_name!"]
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == ""
|
assert ret.elapsedTime == ""
|
||||||
assert ret.bgColor == 0
|
assert ret.bgColor == 0
|
||||||
assert ret.author.name == "author_name"
|
assert ret.author.name == "author_name"
|
||||||
|
|||||||
94
tests/test_default_processor2.py
Normal file
94
tests/test_default_processor2.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import asyncio
|
||||||
|
import pytchat
|
||||||
|
from concurrent.futures import CancelledError
|
||||||
|
from pytchat.core_multithread.livechat import LiveChat
|
||||||
|
from pytchat.core_async.livechat import LiveChatAsync
|
||||||
|
|
||||||
|
cases = [
|
||||||
|
{
|
||||||
|
"video_id":"1X7oL0hDnMg", "seektime":1620,
|
||||||
|
"result1":{'textMessage': 84},
|
||||||
|
"result2":{'': 83, 'MODERATOR': 1}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"video_id":"Hj-wnLIYKjw", "seektime":420,
|
||||||
|
"result1":{'superChat': 1, 'newSponsor': 6, 'textMessage': 63, 'donation': 1},
|
||||||
|
"result2":{'': 59, 'MEMBER': 12}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"video_id":"S8dmq5YIUoc", "seektime":3,
|
||||||
|
"result1":{'textMessage': 86},
|
||||||
|
"result2":{'': 62, 'MEMBER': 21, 'OWNER': 2, 'VERIFIED': 1}
|
||||||
|
},{
|
||||||
|
"video_id":"yLrstz80MKs", "seektime":30,
|
||||||
|
"result1":{'superSticker': 8, 'superChat': 2, 'textMessage': 67},
|
||||||
|
"result2":{'': 73, 'MEMBER': 4}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_archived_stream():
|
||||||
|
for case in cases:
|
||||||
|
stream = pytchat.create(video_id=case["video_id"], seektime=case["seektime"])
|
||||||
|
while stream.is_alive():
|
||||||
|
chat = stream.get()
|
||||||
|
agg1 = {}
|
||||||
|
agg2 = {}
|
||||||
|
for c in chat.items:
|
||||||
|
if c.type in agg1:
|
||||||
|
agg1[c.type] += 1
|
||||||
|
else:
|
||||||
|
agg1[c.type] = 1
|
||||||
|
if c.author.type in agg2:
|
||||||
|
agg2[c.author.type] += 1
|
||||||
|
else:
|
||||||
|
agg2[c.author.type] = 1
|
||||||
|
break
|
||||||
|
assert agg1 == case["result1"]
|
||||||
|
assert agg2 == case["result2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_archived_stream_multithread():
|
||||||
|
for case in cases:
|
||||||
|
stream = LiveChat(video_id=case["video_id"], seektime=case["seektime"])
|
||||||
|
while stream.is_alive():
|
||||||
|
chat = stream.get()
|
||||||
|
agg1 = {}
|
||||||
|
agg2 = {}
|
||||||
|
for c in chat.items:
|
||||||
|
if c.type in agg1:
|
||||||
|
agg1[c.type] += 1
|
||||||
|
else:
|
||||||
|
agg1[c.type] = 1
|
||||||
|
if c.author.type in agg2:
|
||||||
|
agg2[c.author.type] += 1
|
||||||
|
else:
|
||||||
|
agg2[c.author.type] = 1
|
||||||
|
break
|
||||||
|
assert agg1 == case["result1"]
|
||||||
|
assert agg2 == case["result2"]
|
||||||
|
|
||||||
|
def test_async_live_stream():
|
||||||
|
async def test_loop():
|
||||||
|
for case in cases:
|
||||||
|
stream = LiveChatAsync(video_id=case["video_id"], seektime=case["seektime"])
|
||||||
|
while stream.is_alive():
|
||||||
|
chat = await stream.get()
|
||||||
|
agg1 = {}
|
||||||
|
agg2 = {}
|
||||||
|
for c in chat.items:
|
||||||
|
if c.type in agg1:
|
||||||
|
agg1[c.type] += 1
|
||||||
|
else:
|
||||||
|
agg1[c.type] = 1
|
||||||
|
if c.author.type in agg2:
|
||||||
|
agg2[c.author.type] += 1
|
||||||
|
else:
|
||||||
|
agg2[c.author.type] = 1
|
||||||
|
break
|
||||||
|
assert agg1 == case["result1"]
|
||||||
|
assert agg2 == case["result2"]
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(test_loop())
|
||||||
|
except CancelledError:
|
||||||
|
assert True
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import json
|
|
||||||
from pytchat.tool.extract import duplcheck
|
|
||||||
from pytchat.tool.extract import parser
|
|
||||||
from pytchat.tool.extract.block import Block
|
|
||||||
from pytchat.tool.extract.duplcheck import _dump
|
|
||||||
|
|
||||||
|
|
||||||
def _open_file(path):
|
|
||||||
with open(path, mode='r', encoding='utf-8') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
def test_overlap():
|
|
||||||
"""
|
|
||||||
test overlap data
|
|
||||||
operation : [0] [2] [3] [4] -> last :align to end
|
|
||||||
[1] , [5] -> no change
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def load_chatdata(filename):
|
|
||||||
return parser.parse(
|
|
||||||
json.loads(_open_file(
|
|
||||||
"tests/testdata/extract_duplcheck/overlap/" + filename))
|
|
||||||
)[1]
|
|
||||||
|
|
||||||
blocks = (
|
|
||||||
Block(first=0, last=12771, end=9890,
|
|
||||||
chat_data=load_chatdata("dp0-0.json")),
|
|
||||||
Block(first=9890, last=15800, end=20244,
|
|
||||||
chat_data=load_chatdata("dp0-1.json")),
|
|
||||||
Block(first=20244, last=45146, end=32476,
|
|
||||||
chat_data=load_chatdata("dp0-2.json")),
|
|
||||||
Block(first=32476, last=50520, end=41380,
|
|
||||||
chat_data=load_chatdata("dp0-3.json")),
|
|
||||||
Block(first=41380, last=62875, end=52568,
|
|
||||||
chat_data=load_chatdata("dp0-4.json")),
|
|
||||||
Block(first=52568, last=62875, end=54000,
|
|
||||||
chat_data=load_chatdata("dp0-5.json"), is_last=True)
|
|
||||||
)
|
|
||||||
result = duplcheck.remove_overlap(blocks)
|
|
||||||
# dp0-0.json has item offset time is 9890 (equals block[0].end = block[1].first),
|
|
||||||
# but must be aligne to the most close and smaller value:9779.
|
|
||||||
assert result[0].last == 9779
|
|
||||||
|
|
||||||
assert result[1].last == 15800
|
|
||||||
|
|
||||||
assert result[2].last == 32196
|
|
||||||
|
|
||||||
assert result[3].last == 41116
|
|
||||||
|
|
||||||
assert result[4].last == 52384
|
|
||||||
|
|
||||||
# the last block must be always added to result.
|
|
||||||
assert result[5].last == 62875
|
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_head():
|
|
||||||
|
|
||||||
def load_chatdata(filename):
|
|
||||||
return parser.parse(
|
|
||||||
json.loads(_open_file(
|
|
||||||
"tests/testdata/extract_duplcheck/head/" + filename))
|
|
||||||
)[1]
|
|
||||||
|
|
||||||
"""
|
|
||||||
test duplicate head data
|
|
||||||
operation : [0] , [1] -> discard [0]
|
|
||||||
[1] , [2] -> discard [1]
|
|
||||||
[2] , [3] -> append [2]
|
|
||||||
[3] , [4] -> discard [3]
|
|
||||||
[4] , [5] -> append [4]
|
|
||||||
append [5]
|
|
||||||
|
|
||||||
result : [2] , [4] , [5]
|
|
||||||
"""
|
|
||||||
|
|
||||||
# chat data offsets are ignored.
|
|
||||||
blocks = (
|
|
||||||
Block(first=0, last=2500, chat_data=load_chatdata("dp0-0.json")),
|
|
||||||
Block(first=0, last=38771, chat_data=load_chatdata("dp0-1.json")),
|
|
||||||
Block(first=0, last=45146, chat_data=load_chatdata("dp0-2.json")),
|
|
||||||
Block(first=20244, last=60520, chat_data=load_chatdata("dp0-3.json")),
|
|
||||||
Block(first=20244, last=62875, chat_data=load_chatdata("dp0-4.json")),
|
|
||||||
Block(first=52568, last=62875, chat_data=load_chatdata("dp0-5.json"))
|
|
||||||
)
|
|
||||||
_dump(blocks)
|
|
||||||
result = duplcheck.remove_duplicate_head(blocks)
|
|
||||||
|
|
||||||
assert len(result) == 3
|
|
||||||
assert result[0].first == blocks[2].first
|
|
||||||
assert result[0].last == blocks[2].last
|
|
||||||
assert result[1].first == blocks[4].first
|
|
||||||
assert result[1].last == blocks[4].last
|
|
||||||
assert result[2].first == blocks[5].first
|
|
||||||
assert result[2].last == blocks[5].last
|
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_tail():
|
|
||||||
"""
|
|
||||||
test duplicate tail data
|
|
||||||
operation : append [0]
|
|
||||||
[0] , [1] -> discard [1]
|
|
||||||
[1] , [2] -> append [2]
|
|
||||||
[2] , [3] -> discard [3]
|
|
||||||
[3] , [4] -> append [4]
|
|
||||||
[4] , [5] -> discard [5]
|
|
||||||
|
|
||||||
result : [0] , [2] , [4]
|
|
||||||
"""
|
|
||||||
def load_chatdata(filename):
|
|
||||||
return parser.parse(
|
|
||||||
json.loads(_open_file(
|
|
||||||
"tests/testdata/extract_duplcheck/head/" + filename))
|
|
||||||
)[1]
|
|
||||||
# chat data offsets are ignored.
|
|
||||||
blocks = (
|
|
||||||
Block(first=0, last=2500, chat_data=load_chatdata("dp0-0.json")),
|
|
||||||
Block(first=1500, last=2500, chat_data=load_chatdata("dp0-1.json")),
|
|
||||||
Block(first=10000, last=45146, chat_data=load_chatdata("dp0-2.json")),
|
|
||||||
Block(first=20244, last=45146, chat_data=load_chatdata("dp0-3.json")),
|
|
||||||
Block(first=20244, last=62875, chat_data=load_chatdata("dp0-4.json")),
|
|
||||||
Block(first=52568, last=62875, chat_data=load_chatdata("dp0-5.json"))
|
|
||||||
)
|
|
||||||
|
|
||||||
result = duplcheck.remove_duplicate_tail(blocks)
|
|
||||||
_dump(result)
|
|
||||||
assert len(result) == 3
|
|
||||||
assert result[0].first == blocks[0].first
|
|
||||||
assert result[0].last == blocks[0].last
|
|
||||||
assert result[1].first == blocks[2].first
|
|
||||||
assert result[1].last == blocks[2].last
|
|
||||||
assert result[2].first == blocks[4].first
|
|
||||||
assert result[2].last == blocks[4].last
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from pytchat.tool.extract import parser
|
|
||||||
from pytchat.tool.extract.block import Block
|
|
||||||
from pytchat.tool.extract.patch import Patch, split
|
|
||||||
|
|
||||||
|
|
||||||
def _open_file(path):
|
|
||||||
with open(path, mode='r', encoding='utf-8') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
def load_chatdata(filename):
|
|
||||||
return parser.parse(
|
|
||||||
json.loads(_open_file("tests/testdata/fetch_patch/" + filename))
|
|
||||||
)[1]
|
|
||||||
|
|
||||||
|
|
||||||
def test_split_0():
|
|
||||||
"""
|
|
||||||
Normal case
|
|
||||||
|
|
||||||
~~~~~~ before ~~~~~~
|
|
||||||
|
|
||||||
@parent_block (# = already fetched)
|
|
||||||
|
|
||||||
first last end
|
|
||||||
|########----------------------------------------|
|
|
||||||
|
|
||||||
|
|
||||||
@child_block
|
|
||||||
|
|
||||||
first = last = 0 end (=parent_end)
|
|
||||||
| |
|
|
||||||
|
|
||||||
|
|
||||||
@fetched patch
|
|
||||||
|-- patch --|
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
|
||||||
V
|
|
||||||
|
|
||||||
~~~~~~ after ~~~~~~
|
|
||||||
|
|
||||||
|
|
||||||
@parent_block
|
|
||||||
|
|
||||||
first last end (after split)
|
|
||||||
|########------------|
|
|
||||||
|
|
||||||
@child_block
|
|
||||||
first last end
|
|
||||||
|###########---------------|
|
|
||||||
|
|
||||||
@fetched patch
|
|
||||||
|-- patch --|
|
|
||||||
"""
|
|
||||||
parent = Block(first=0, last=4000, end=60000,
|
|
||||||
continuation='parent', during_split=True)
|
|
||||||
child = Block(first=0, last=0, end=60000,
|
|
||||||
continuation='mean', during_split=True)
|
|
||||||
patch = Patch(chats=load_chatdata('pt0-5.json'),
|
|
||||||
first=32500, last=34000, continuation='patch')
|
|
||||||
|
|
||||||
split(parent, child, patch)
|
|
||||||
|
|
||||||
assert child.continuation == 'patch'
|
|
||||||
assert parent.last < child.first
|
|
||||||
assert parent.end == child.first
|
|
||||||
assert child.first < child.last
|
|
||||||
assert child.last < child.end
|
|
||||||
assert parent.during_split is False
|
|
||||||
assert child.during_split is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_split_1():
|
|
||||||
"""patch.first <= parent_block.last
|
|
||||||
|
|
||||||
While awaiting at run()->asyncdl._fetch()
|
|
||||||
fetching parent_block proceeds,
|
|
||||||
and parent.block.last exceeds patch.first.
|
|
||||||
|
|
||||||
In this case, fetched patch is all discarded,
|
|
||||||
and worker searches other processing block again.
|
|
||||||
|
|
||||||
~~~~~~ before ~~~~~~
|
|
||||||
|
|
||||||
patch.first
|
|
||||||
first | last end
|
|
||||||
|####################|#####|---------------------|
|
|
||||||
^
|
|
||||||
@child_block
|
|
||||||
first = last = 0 end (=parent_end)
|
|
||||||
| |
|
|
||||||
|
|
||||||
@fetched patch
|
|
||||||
|-- patch --|
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
|
||||||
V
|
|
||||||
|
|
||||||
~~~~~~ after ~~~~~~
|
|
||||||
|
|
||||||
@parent_block
|
|
||||||
first last end
|
|
||||||
|###########################|--------------------|
|
|
||||||
|
|
||||||
@child_block
|
|
||||||
|
|
||||||
.............. -> discard all data
|
|
||||||
|
|
||||||
"""
|
|
||||||
parent = Block(first=0, last=33000, end=60000, continuation='parent', during_split=True)
|
|
||||||
child = Block(first=0, last=0, end=60000, continuation='mean', during_split=True)
|
|
||||||
patch = Patch(chats=load_chatdata('pt0-5.json'),
|
|
||||||
first=32500, last=34000, continuation='patch')
|
|
||||||
|
|
||||||
split(parent, child, patch)
|
|
||||||
|
|
||||||
assert parent.last == 33000 # no change
|
|
||||||
assert parent.end == 60000 # no change
|
|
||||||
assert child.continuation is None
|
|
||||||
assert parent.during_split is False
|
|
||||||
assert child.during_split is True # exclude during_split sequence
|
|
||||||
|
|
||||||
|
|
||||||
def test_split_2():
|
|
||||||
"""child_block.end < patch.last:
|
|
||||||
|
|
||||||
Case the last offset of patch exceeds child_block.end.
|
|
||||||
In this case, remove overlapped data of patch.
|
|
||||||
|
|
||||||
~~~~~~ before ~~~~~~
|
|
||||||
|
|
||||||
@parent_block (# = already fetched)
|
|
||||||
first last end (before split)
|
|
||||||
|########------------------------------|
|
|
||||||
|
|
||||||
@child_block
|
|
||||||
first = last = 0 end (=parent_end)
|
|
||||||
| |
|
|
||||||
|
|
||||||
continuation:succeed from patch
|
|
||||||
|
|
||||||
@fetched patch
|
|
||||||
|-------- patch --------|
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
|
||||||
V
|
|
||||||
|
|
||||||
~~~~~~ after ~~~~~~
|
|
||||||
|
|
||||||
@parent_block
|
|
||||||
first last end (after split)
|
|
||||||
|########------------|
|
|
||||||
|
|
||||||
@child_block old patch.end
|
|
||||||
first last=end |
|
|
||||||
|#################|...... cut extra data.
|
|
||||||
^
|
|
||||||
continuation : None (extract complete)
|
|
||||||
|
|
||||||
@fetched patch
|
|
||||||
|-------- patch --------|
|
|
||||||
"""
|
|
||||||
parent = Block(first=0, last=4000, end=33500, continuation='parent', during_split=True)
|
|
||||||
child = Block(first=0, last=0, end=33500, continuation='mean', during_split=True)
|
|
||||||
patch = Patch(chats=load_chatdata('pt0-5.json'),
|
|
||||||
first=32500, last=34000, continuation='patch')
|
|
||||||
|
|
||||||
split(parent, child, patch)
|
|
||||||
|
|
||||||
assert child.continuation is None
|
|
||||||
assert parent.last < child.first
|
|
||||||
assert parent.end == child.first
|
|
||||||
assert child.first < child.last
|
|
||||||
assert child.last < child.end
|
|
||||||
assert child.continuation is None
|
|
||||||
assert parent.during_split is False
|
|
||||||
assert child.during_split is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_split_none():
|
|
||||||
"""patch.last <= parent_block.last
|
|
||||||
|
|
||||||
While awaiting at run()->asyncdl._fetch()
|
|
||||||
fetching parent_block proceeds,
|
|
||||||
and parent.block.last exceeds patch.first.
|
|
||||||
|
|
||||||
In this case, fetched patch is all discarded,
|
|
||||||
and worker searches other processing block again.
|
|
||||||
|
|
||||||
~~~~~~ before ~~~~~~
|
|
||||||
|
|
||||||
patch.first
|
|
||||||
first | last end
|
|
||||||
|####################|###################|-------|
|
|
||||||
^
|
|
||||||
@child_block
|
|
||||||
first = last = 0 end (=parent_end)
|
|
||||||
| |
|
|
||||||
|
|
||||||
@fetched patch
|
|
||||||
|-- patch --|
|
|
||||||
patch.last < parent_block.last .
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
|
||||||
V
|
|
||||||
|
|
||||||
~~~~~~ after ~~~~~~
|
|
||||||
|
|
||||||
@parent_block
|
|
||||||
first last end (before split)
|
|
||||||
|########################################|-------|
|
|
||||||
|
|
||||||
@child_block
|
|
||||||
|
|
||||||
............ -> discard all data.
|
|
||||||
|
|
||||||
"""
|
|
||||||
parent = Block(first=0, last=40000, end=60000, continuation='parent', during_split=True)
|
|
||||||
child = Block(first=0, last=0, end=60000, continuation='mean', during_split=True)
|
|
||||||
patch = Patch(chats=load_chatdata('pt0-5.json'),
|
|
||||||
first=32500, last=34000, continuation='patch')
|
|
||||||
|
|
||||||
split(parent, child, patch)
|
|
||||||
|
|
||||||
assert parent.last == 40000 # no change
|
|
||||||
assert parent.end == 60000 # no change
|
|
||||||
assert child.continuation is None
|
|
||||||
assert parent.during_split is False
|
|
||||||
assert child.during_split is True # exclude during_split sequence
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from pytchat.util.extract_video_id import extract_video_id
|
from pytchat.util import extract_video_id
|
||||||
from pytchat.exceptions import InvalidVideoIdException
|
from pytchat.exceptions import InvalidVideoIdException
|
||||||
|
|
||||||
VALID_TEST_PATTERNS = (
|
VALID_TEST_PATTERNS = (
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
from pytest_httpx import HTTPXMock
|
|
||||||
from concurrent.futures import CancelledError
|
|
||||||
from pytchat.core_multithread.livechat import LiveChat
|
|
||||||
from pytchat.core_async.livechat import LiveChatAsync
|
|
||||||
from pytchat.exceptions import ResponseContextError
|
|
||||||
|
|
||||||
|
|
||||||
def _open_file(path):
|
|
||||||
with open(path, mode='r', encoding='utf-8') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
def add_response_file(httpx_mock: HTTPXMock, jsonfile_path: str):
|
|
||||||
testdata = json.loads(_open_file(jsonfile_path))
|
|
||||||
httpx_mock.add_response(json=testdata)
|
|
||||||
|
|
||||||
|
|
||||||
def test_async(httpx_mock: HTTPXMock):
|
|
||||||
add_response_file(httpx_mock, 'tests/testdata/paramgen_firstread.json')
|
|
||||||
|
|
||||||
async def test_loop():
|
|
||||||
try:
|
|
||||||
chat = LiveChatAsync(video_id='__test_id__')
|
|
||||||
_ = await chat.get()
|
|
||||||
assert chat.is_alive()
|
|
||||||
chat.terminate()
|
|
||||||
assert not chat.is_alive()
|
|
||||||
except ResponseContextError:
|
|
||||||
assert False
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(test_loop())
|
|
||||||
except CancelledError:
|
|
||||||
assert True
|
|
||||||
|
|
||||||
|
|
||||||
def test_multithread(httpx_mock: HTTPXMock):
|
|
||||||
add_response_file(httpx_mock, 'tests/testdata/paramgen_firstread.json')
|
|
||||||
try:
|
|
||||||
chat = LiveChat(video_id='__test_id__')
|
|
||||||
_ = chat.get()
|
|
||||||
assert chat.is_alive()
|
|
||||||
chat.terminate()
|
|
||||||
assert not chat.is_alive()
|
|
||||||
except ResponseContextError:
|
|
||||||
assert False
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
from pytest_httpx import HTTPXMock
|
|
||||||
from concurrent.futures import CancelledError
|
|
||||||
from pytchat.core_multithread.livechat import LiveChat
|
|
||||||
from pytchat.core_async.livechat import LiveChatAsync
|
|
||||||
from pytchat.processors.dummy_processor import DummyProcessor
|
|
||||||
|
|
||||||
|
|
||||||
def _open_file(path):
|
|
||||||
with open(path, mode='r', encoding='utf-8') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
def add_response_file(httpx_mock: HTTPXMock, jsonfile_path: str):
|
|
||||||
testdata = json.loads(_open_file(jsonfile_path))
|
|
||||||
httpx_mock.add_response(json=testdata)
|
|
||||||
|
|
||||||
|
|
||||||
def test_async_live_stream(httpx_mock: HTTPXMock):
|
|
||||||
add_response_file(httpx_mock, 'tests/testdata/test_stream.json')
|
|
||||||
|
|
||||||
async def test_loop():
|
|
||||||
chat = LiveChatAsync(video_id='__test_id__', processor=DummyProcessor())
|
|
||||||
chats = await chat.get()
|
|
||||||
rawdata = chats[0]["chatdata"]
|
|
||||||
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTextMessageRenderer"
|
|
||||||
assert list(rawdata[1]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTextMessageRenderer"
|
|
||||||
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatPlaceholderItemRenderer"
|
|
||||||
assert list(rawdata[3]["addLiveChatTickerItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTickerPaidMessageItemRenderer"
|
|
||||||
assert list(rawdata[4]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatPaidMessageRenderer"
|
|
||||||
assert list(rawdata[5]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatPaidStickerRenderer"
|
|
||||||
assert list(rawdata[6]["addLiveChatTickerItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTickerSponsorItemRenderer"
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(test_loop())
|
|
||||||
except CancelledError:
|
|
||||||
assert True
|
|
||||||
|
|
||||||
|
|
||||||
def test_async_replay_stream(httpx_mock: HTTPXMock):
|
|
||||||
add_response_file(httpx_mock, 'tests/testdata/finished_live.json')
|
|
||||||
add_response_file(httpx_mock, 'tests/testdata/chatreplay.json')
|
|
||||||
|
|
||||||
async def test_loop():
|
|
||||||
chat = LiveChatAsync(video_id='__test_id__', processor=DummyProcessor())
|
|
||||||
chats = await chat.get()
|
|
||||||
rawdata = chats[0]["chatdata"]
|
|
||||||
# assert fetching replaychat data
|
|
||||||
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTextMessageRenderer"
|
|
||||||
assert list(rawdata[14]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatPaidMessageRenderer"
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(test_loop())
|
|
||||||
except CancelledError:
|
|
||||||
assert True
|
|
||||||
|
|
||||||
|
|
||||||
def test_async_force_replay(httpx_mock: HTTPXMock):
|
|
||||||
add_response_file(httpx_mock, 'tests/testdata/test_stream.json')
|
|
||||||
add_response_file(httpx_mock, 'tests/testdata/chatreplay.json')
|
|
||||||
|
|
||||||
async def test_loop():
|
|
||||||
chat = LiveChatAsync(
|
|
||||||
video_id='__test_id__', processor=DummyProcessor(), force_replay=True)
|
|
||||||
chats = await chat.get()
|
|
||||||
rawdata = chats[0]["chatdata"]
|
|
||||||
# assert fetching replaychat data
|
|
||||||
assert list(rawdata[14]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatPaidMessageRenderer"
|
|
||||||
# assert not mix livechat data
|
|
||||||
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] != "liveChatPlaceholderItemRenderer"
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(test_loop())
|
|
||||||
except CancelledError:
|
|
||||||
assert True
|
|
||||||
|
|
||||||
|
|
||||||
def test_multithread_live_stream(httpx_mock: HTTPXMock):
|
|
||||||
add_response_file(httpx_mock, 'tests/testdata/test_stream.json')
|
|
||||||
chat = LiveChat(video_id='__test_id__', processor=DummyProcessor())
|
|
||||||
chats = chat.get()
|
|
||||||
rawdata = chats[0]["chatdata"]
|
|
||||||
# assert fetching livachat data
|
|
||||||
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTextMessageRenderer"
|
|
||||||
assert list(rawdata[1]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTextMessageRenderer"
|
|
||||||
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatPlaceholderItemRenderer"
|
|
||||||
assert list(rawdata[3]["addLiveChatTickerItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTickerPaidMessageItemRenderer"
|
|
||||||
assert list(rawdata[4]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatPaidMessageRenderer"
|
|
||||||
assert list(rawdata[5]["addChatItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatPaidStickerRenderer"
|
|
||||||
assert list(rawdata[6]["addLiveChatTickerItemAction"]["item"].keys())[
|
|
||||||
0] == "liveChatTickerSponsorItemRenderer"
|
|
||||||
chat.terminate()
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from pytchat.paramgen import liveparam
|
|
||||||
|
|
||||||
def test_liveparam_0(mocker):
|
|
||||||
_ts1= 1546268400
|
|
||||||
param = liveparam._build("01234567890",
|
|
||||||
*([_ts1*1000000 for i in range(5)]), topchat_only=False)
|
|
||||||
test_param="0ofMyANcGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09KIC41tWqyt8CQAFKC1CAuNbVqsrfAlgDUIC41tWqyt8CWIC41tWqyt8CaAGCAQIIAZoBAKABgLjW1arK3wI%3D"
|
|
||||||
assert test_param == param
|
|
||||||
@@ -12,29 +12,23 @@ def _open_file(path):
|
|||||||
|
|
||||||
|
|
||||||
def test_finishedlive(*mock):
|
def test_finishedlive(*mock):
|
||||||
'''配信が終了した動画を正しく処理できるか'''
|
|
||||||
_text = _open_file('tests/testdata/finished_live.json')
|
_text = _open_file('tests/testdata/finished_live.json')
|
||||||
_text = json.loads(_text)
|
_text = json.loads(_text)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parser.parse(parser.get_contents(_text))
|
parser.parse(parser.get_contents(_text)[0])
|
||||||
assert False
|
assert False
|
||||||
except NoContents:
|
except NoContents:
|
||||||
assert True
|
assert True
|
||||||
|
|
||||||
|
|
||||||
def test_parsejson(*mock):
|
def test_parsejson(*mock):
|
||||||
'''jsonを正常にパースできるか'''
|
|
||||||
_text = _open_file('tests/testdata/paramgen_firstread.json')
|
_text = _open_file('tests/testdata/paramgen_firstread.json')
|
||||||
_text = json.loads(_text)
|
_text = json.loads(_text)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parser.parse(parser.get_contents(_text))
|
s, _ = parser.parse(parser.get_contents(_text)[0])
|
||||||
jsn = _text
|
assert s['timeoutMs'] == 5035
|
||||||
timeout = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["timeoutMs"]
|
assert s['continuation'] == "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%3D"
|
||||||
continuation = jsn["response"]["continuationContents"]["liveChatContinuation"][
|
|
||||||
"continuations"][0]["timedContinuationData"]["continuation"]
|
|
||||||
assert timeout == 5035
|
|
||||||
assert continuation == "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%3D"
|
|
||||||
except Exception:
|
except Exception:
|
||||||
assert False
|
assert False
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def test_speed_1(mocker):
|
|||||||
|
|
||||||
_json = _open_file("tests/testdata/speed/speedtest_normal.json")
|
_json = _open_file("tests/testdata/speed/speedtest_normal.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 10,
|
"timeout": 10,
|
||||||
@@ -32,7 +32,7 @@ def test_speed_2(mocker):
|
|||||||
|
|
||||||
_json = _open_file("tests/testdata/speed/speedtest_undefined.json")
|
_json = _open_file("tests/testdata/speed/speedtest_undefined.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 10,
|
"timeout": 10,
|
||||||
@@ -49,7 +49,7 @@ def test_speed_3(mocker):
|
|||||||
|
|
||||||
_json = _open_file("tests/testdata/speed/speedtest_empty.json")
|
_json = _open_file("tests/testdata/speed/speedtest_empty.json")
|
||||||
|
|
||||||
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
|
_, chatdata = parser.parse(parser.get_contents(json.loads(_json))[0])
|
||||||
data = {
|
data = {
|
||||||
"video_id": "",
|
"video_id": "",
|
||||||
"timeout": 10,
|
"timeout": 10,
|
||||||
|
|||||||
16
tests/test_speed_calculator2.py
Normal file
16
tests/test_speed_calculator2.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import json
|
||||||
|
import pytchat
|
||||||
|
from pytchat.parser.live import Parser
|
||||||
|
from pytchat.processors.speed.calculator import SpeedCalculator
|
||||||
|
|
||||||
|
parser = Parser(is_replay=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_speed_1():
|
||||||
|
stream = pytchat.create("Hj-wnLIYKjw", seektime = 6000,processor=SpeedCalculator())
|
||||||
|
while stream.is_alive():
|
||||||
|
speed = stream.get()
|
||||||
|
assert speed > 100
|
||||||
|
break
|
||||||
|
test_speed_1()
|
||||||
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
from json.decoder import JSONDecodeError
|
|
||||||
from pytchat.tool.videoinfo import VideoInfo
|
|
||||||
from pytchat.exceptions import InvalidVideoIdException, PatternUnmatchError
|
|
||||||
|
|
||||||
|
|
||||||
def _open_file(path):
|
|
||||||
with open(path, mode='r', encoding='utf-8') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
def _set_test_data(filepath, mocker):
|
|
||||||
_text = _open_file(filepath)
|
|
||||||
response_mock = mocker.Mock()
|
|
||||||
response_mock.status_code = 200
|
|
||||||
response_mock.text = _text
|
|
||||||
mocker.patch('httpx.get').return_value = response_mock
|
|
||||||
|
|
||||||
|
|
||||||
def test_archived_page(mocker):
|
|
||||||
_set_test_data('tests/testdata/videoinfo/archived_page.txt', mocker)
|
|
||||||
info = VideoInfo('__test_id__')
|
|
||||||
actual_thumbnail_url = 'https://i.ytimg.com/vi/fzI9FNjXQ0o/hqdefault.jpg'
|
|
||||||
assert info.video_id == '__test_id__'
|
|
||||||
assert info.get_channel_name() == 'GitHub'
|
|
||||||
assert info.get_thumbnail() == actual_thumbnail_url
|
|
||||||
assert info.get_title() == 'GitHub Arctic Code Vault'
|
|
||||||
assert info.get_channel_id() == 'UC7c3Kb6jYCRj4JOHHZTxKsQ'
|
|
||||||
assert info.get_duration() == 148
|
|
||||||
|
|
||||||
|
|
||||||
def test_live_page(mocker):
|
|
||||||
_set_test_data('tests/testdata/videoinfo/live_page.txt', mocker)
|
|
||||||
info = VideoInfo('__test_id__')
|
|
||||||
'''live page: duration==0'''
|
|
||||||
assert info.get_duration() == 0
|
|
||||||
assert info.video_id == '__test_id__'
|
|
||||||
assert info.get_channel_name() == 'BGM channel'
|
|
||||||
assert info.get_thumbnail() == \
|
|
||||||
'https://i.ytimg.com/vi/fEvM-OUbaKs/hqdefault_live.jpg'
|
|
||||||
assert info.get_title() == (
|
|
||||||
'Coffee Jazz Music - Chill Out Lounge Jazz Music Radio'
|
|
||||||
' - 24/7 Live Stream - Slow Jazz')
|
|
||||||
assert info.get_channel_id() == 'UCQINXHZqCU5i06HzxRkujfg'
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_video_id(mocker):
|
|
||||||
'''Test case invalid video_id is specified.'''
|
|
||||||
_set_test_data(
|
|
||||||
'tests/testdata/videoinfo/invalid_video_id_page.txt', mocker)
|
|
||||||
try:
|
|
||||||
_ = VideoInfo('__test_id__')
|
|
||||||
assert False
|
|
||||||
except InvalidVideoIdException:
|
|
||||||
assert True
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_info(mocker):
|
|
||||||
'''Test case the video page has renderer, but no info.'''
|
|
||||||
_set_test_data(
|
|
||||||
'tests/testdata/videoinfo/no_info_page.txt', mocker)
|
|
||||||
info = VideoInfo('__test_id__')
|
|
||||||
assert info.video_id == '__test_id__'
|
|
||||||
assert info.get_channel_name() is None
|
|
||||||
assert info.get_thumbnail() is None
|
|
||||||
assert info.get_title() is None
|
|
||||||
assert info.get_channel_id() is None
|
|
||||||
assert info.get_duration() is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_collapsed_data(mocker):
|
|
||||||
'''Test case the video page's info is collapsed.'''
|
|
||||||
_set_test_data(
|
|
||||||
'tests/testdata/videoinfo/collapsed_page.txt', mocker)
|
|
||||||
try:
|
|
||||||
_ = VideoInfo('__test_id__')
|
|
||||||
assert False
|
|
||||||
except JSONDecodeError:
|
|
||||||
assert True
|
|
||||||
|
|
||||||
|
|
||||||
def test_pattern_unmatch(mocker):
|
|
||||||
'''Test case the pattern for extraction is unmatched.'''
|
|
||||||
_set_test_data(
|
|
||||||
'tests/testdata/videoinfo/pattern_unmatch.txt', mocker)
|
|
||||||
try:
|
|
||||||
_ = VideoInfo('__test_id__')
|
|
||||||
assert False
|
|
||||||
except PatternUnmatchError:
|
|
||||||
assert True
|
|
||||||
|
|
||||||
|
|
||||||
def test_extradata_handling(mocker):
|
|
||||||
'''Test case the extracted data are JSON lines.'''
|
|
||||||
_set_test_data(
|
|
||||||
'tests/testdata/videoinfo/extradata_page.txt', mocker)
|
|
||||||
try:
|
|
||||||
_ = VideoInfo('__test_id__')
|
|
||||||
assert True
|
|
||||||
except JSONDecodeError as e:
|
|
||||||
print(e.doc)
|
|
||||||
assert False
|
|
||||||
6050
tests/testdata/calculator/superchat_0.json
vendored
6050
tests/testdata/calculator/superchat_0.json
vendored
File diff suppressed because it is too large
Load Diff
160
tests/testdata/calculator/text_only.json
vendored
160
tests/testdata/calculator/text_only.json
vendored
@@ -1,89 +1,87 @@
|
|||||||
{
|
{
|
||||||
"response": {
|
|
||||||
"responseContext": {
|
"responseContext": {
|
||||||
"webResponseContextExtensionData": ""
|
"webResponseContextExtensionData": ""
|
||||||
},
|
},
|
||||||
"continuationContents": {
|
"continuationContents": {
|
||||||
"liveChatContinuation": {
|
"liveChatContinuation": {
|
||||||
"continuations": [
|
"continuations": [
|
||||||
{
|
|
||||||
"invalidationContinuationData": {
|
|
||||||
"invalidationId": {
|
|
||||||
"objectSource": 1000,
|
|
||||||
"objectId": "___objectId___",
|
|
||||||
"topic": "chat~00000000000~0000000",
|
|
||||||
"subscribeToGcmTopics": true,
|
|
||||||
"protoCreationTimestampMs": "1577804400000"
|
|
||||||
},
|
|
||||||
"timeoutMs": 10000,
|
|
||||||
"continuation": "___continuation___"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"replayChatItemAction": {
|
|
||||||
"actions": [
|
|
||||||
{
|
{
|
||||||
"addChatItemAction": {
|
"invalidationContinuationData": {
|
||||||
"item": {
|
"invalidationId": {
|
||||||
"liveChatTextMessageRenderer": {
|
"objectSource": 1000,
|
||||||
"message": {
|
"objectId": "___objectId___",
|
||||||
"runs": [
|
"topic": "chat~00000000000~0000000",
|
||||||
{
|
"subscribeToGcmTopics": true,
|
||||||
"text": "dummy_message"
|
"protoCreationTimestampMs": "1577804400000"
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"authorName": {
|
"timeoutMs": 10000,
|
||||||
"simpleText": "author_name"
|
"continuation": "___continuation___"
|
||||||
},
|
}
|
||||||
"authorPhoto": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 32,
|
|
||||||
"height": 32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"liveChatItemContextMenuEndpoint": {
|
|
||||||
"params": "___params___"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "dummy_id",
|
|
||||||
"timestampUsec": 0,
|
|
||||||
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
|
|
||||||
"contextMenuAccessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "コメントの操作"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestampText": {
|
|
||||||
"simpleText": "0:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"clientId": "dummy_client_id"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"videoOffsetTimeMsec": "10000"
|
"actions": [
|
||||||
}
|
{
|
||||||
}
|
"replayChatItemAction": {
|
||||||
]
|
"actions": [
|
||||||
}
|
{
|
||||||
|
"addChatItemAction": {
|
||||||
|
"item": {
|
||||||
|
"liveChatTextMessageRenderer": {
|
||||||
|
"message": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "dummy_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"authorName": {
|
||||||
|
"simpleText": "author_name"
|
||||||
|
},
|
||||||
|
"authorPhoto": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 32,
|
||||||
|
"height": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"liveChatItemContextMenuEndpoint": {
|
||||||
|
"params": "___params___"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "dummy_id",
|
||||||
|
"timestampUsec": 0,
|
||||||
|
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestampText": {
|
||||||
|
"simpleText": "0:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clientId": "dummy_client_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"videoOffsetTimeMsec": "10000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
6172
tests/testdata/chatreplay.json
vendored
6172
tests/testdata/chatreplay.json
vendored
File diff suppressed because it is too large
Load Diff
3679
tests/testdata/compatible/newSponsor.json
vendored
3679
tests/testdata/compatible/newSponsor.json
vendored
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"st": 100
|
"st": 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"response": {
|
|
||||||
"responseContext": {
|
"responseContext": {
|
||||||
"serviceTrackingParams": [
|
"serviceTrackingParams": [
|
||||||
{
|
{
|
||||||
@@ -1805,7 +1805,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trackingParams": "CAAQ0b4BIhMI1LbVw_aa5QIV2cxMAh2AtAj8"
|
"trackingParams": "CAAQ0b4BIhMI1LbVw_aa5QIV2cxMAh2AtAj8"
|
||||||
},
|
,
|
||||||
"url": "\/live_chat\/get_live_chat?continuation=0ofMyAOBAhrKAUNrd1NJUW9ZVlVOdVVsRlpTRlJ1VWt4VFJqQmpURXAzVFc1bFpFTm5FZ1V2YkdsMlpTb25DaGhWUTI1U1VWbElWRzVTVEZOR01HTk1TbmROYm1Wa1EyY1NDMDFETTNkVlNpMUNXRTVGR2tPcXVjRzlBVDBLTzJoMGRIQnpPaTh2ZDNkM0xubHZkWFIxWW1VdVkyOXRMMnhwZG1WZlkyaGhkRDkyUFUxRE0zZFZTaTFDV0U1RkptbHpYM0J2Y0c5MWREMHhJQUklM0QwAUopCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoAUPOaw8P2muUCWANoAYIBAggB",
|
"url": "\/live_chat\/get_live_chat?continuation=0ofMyAOBAhrKAUNrd1NJUW9ZVlVOdVVsRlpTRlJ1VWt4VFJqQmpURXAzVFc1bFpFTm5FZ1V2YkdsMlpTb25DaGhWUTI1U1VWbElWRzVTVEZOR01HTk1TbmROYm1Wa1EyY1NDMDFETTNkVlNpMUNXRTVGR2tPcXVjRzlBVDBLTzJoMGRIQnpPaTh2ZDNkM0xubHZkWFIxWW1VdVkyOXRMMnhwZG1WZlkyaGhkRDkyUFUxRE0zZFZTaTFDV0U1RkptbHpYM0J2Y0c5MWREMHhJQUklM0QwAUopCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoAUPOaw8P2muUCWANoAYIBAggB",
|
||||||
"csn": "PvujXbH0OIazqQHXgJ64DQ",
|
"csn": "PvujXbH0OIazqQHXgJ64DQ",
|
||||||
"endpoint": {
|
"endpoint": {
|
||||||
|
|||||||
541
tests/testdata/compatible/superchat.json
vendored
541
tests/testdata/compatible/superchat.json
vendored
@@ -1,282 +1,281 @@
|
|||||||
{
|
{
|
||||||
"xsrf_token": "QUFFLUhqbWVqWVRTUjhBcGRkNWR5Q2F3VWhxcTd0RkF1UXxBQ3Jtc0trMThFVGVVNTFnWmZucnkwNlJJMEZ2bndUS1I3b2dpLUtTNE92dEgwX3Y0MkNZU2NXdGY1QTMtX09BUGphYUpDQlc1dFhiTm9jLS1sQXVCNHpsdllqcm1id0t1RFBCM3A1b2o3OGt0Yjd6TE5wcmxBbjNyQktjc1lWZ3hjM1RuYk83YkQ0VVN3MGUybjAwSE90SS1YNkxvMUV5YVE=",
|
"xsrf_token": "QUFFLUhqbWVqWVRTUjhBcGRkNWR5Q2F3VWhxcTd0RkF1UXxBQ3Jtc0trMThFVGVVNTFnWmZucnkwNlJJMEZ2bndUS1I3b2dpLUtTNE92dEgwX3Y0MkNZU2NXdGY1QTMtX09BUGphYUpDQlc1dFhiTm9jLS1sQXVCNHpsdllqcm1id0t1RFBCM3A1b2o3OGt0Yjd6TE5wcmxBbjNyQktjc1lWZ3hjM1RuYk83YkQ0VVN3MGUybjAwSE90SS1YNkxvMUV5YVE=",
|
||||||
"timing": {
|
"timing": {
|
||||||
"info": {
|
"info": {
|
||||||
"st": 148
|
"st": 148
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"endpoint": {
|
"urlEndpoint": {
|
||||||
"commandMetadata": {
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
||||||
"webCommandMetadata": {
|
}
|
||||||
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
},
|
||||||
}
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D",
|
||||||
},
|
"csn": "n2STXd2iKZr2gAOt9qvgCg",
|
||||||
"urlEndpoint": {
|
|
||||||
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
"responseContext": {
|
||||||
|
"serviceTrackingParams": [
|
||||||
|
{
|
||||||
|
"service": "CSI",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "GetLiveChat_rid",
|
||||||
|
"value": "0x9290108c05344647"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "c",
|
||||||
|
"value": "WEB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "cver",
|
||||||
|
"value": "2.20191001.04.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "yt_li",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "GFEEDBACK",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "e",
|
||||||
|
"value": "23744176,23788875,23793834,23794620,23804281,23806159,23816483,23819244,23820768,23826780,23827354,23830392,23832125,23835020,23836965,23837741,23837772,23837993,23838235,23839362,23840155,23840217,23841118,23841454,23842630,23842662,23842883,23842986,23843289,23843534,23843767,23845644,9449243,9471239,9474360"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "logged_in",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "GUIDED_HELP",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "logged_in",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "ECATCHER",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "client.name",
|
||||||
|
"value": "WEB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "client.version",
|
||||||
|
"value": "2.20191001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.changelist",
|
||||||
|
"value": "272006966"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.experiments.source_version",
|
||||||
|
"value": "272166268"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.label",
|
||||||
|
"value": "youtube.ytfe.innertube_20190930_5_RC0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.timestamp",
|
||||||
|
"value": "1569863426"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.variants.checksum",
|
||||||
|
"value": "1a800c1a2396906f1cbb7f670d43b6f5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.run.job",
|
||||||
|
"value": "ytfe-innertube-replica-only.ytfe"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"webResponseContextExtensionData": {
|
||||||
|
"ytConfigData": {
|
||||||
|
"csn": "n2STXd2iKZr2gAOt9qvgCg",
|
||||||
|
"visitorData": "CgtPQm1xTmtvNm1Tcyifyc3sBQ%3D%3D"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"continuationContents": {
|
||||||
|
"liveChatContinuation": {
|
||||||
|
"continuations": [
|
||||||
|
{
|
||||||
|
"timedContinuationData": {
|
||||||
|
"timeoutMs": 8860,
|
||||||
|
"continuation": "0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCid_ejQpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DF-ovRpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABtNic0aT75AI%3D"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
"url": "\/live_chat\/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D",
|
"actions": [
|
||||||
"csn": "n2STXd2iKZr2gAOt9qvgCg",
|
{
|
||||||
"response": {
|
"addChatItemAction": {
|
||||||
"responseContext": {
|
"item": {
|
||||||
"serviceTrackingParams": [
|
"liveChatPaidMessageRenderer": {
|
||||||
{
|
"id": "ChwKGkNOblpoTXFrLS1RQ0ZRSU9ZQW9kclhrRXNn",
|
||||||
"service": "CSI",
|
"timestampUsec": "1569940638420061",
|
||||||
"params": [
|
"authorName": {
|
||||||
{
|
"simpleText": "九十九 万"
|
||||||
"key": "GetLiveChat_rid",
|
|
||||||
"value": "0x9290108c05344647"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "c",
|
|
||||||
"value": "WEB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "cver",
|
|
||||||
"value": "2.20191001.04.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "yt_li",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
"authorPhoto": {
|
||||||
"service": "GFEEDBACK",
|
"thumbnails": [
|
||||||
"params": [
|
|
||||||
{
|
|
||||||
"key": "e",
|
|
||||||
"value": "23744176,23788875,23793834,23794620,23804281,23806159,23816483,23819244,23820768,23826780,23827354,23830392,23832125,23835020,23836965,23837741,23837772,23837993,23838235,23839362,23840155,23840217,23841118,23841454,23842630,23842662,23842883,23842986,23843289,23843534,23843767,23845644,9449243,9471239,9474360"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "logged_in",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"service": "GUIDED_HELP",
|
|
||||||
"params": [
|
|
||||||
{
|
|
||||||
"key": "logged_in",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"service": "ECATCHER",
|
|
||||||
"params": [
|
|
||||||
{
|
|
||||||
"key": "client.name",
|
|
||||||
"value": "WEB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "client.version",
|
|
||||||
"value": "2.20191001"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.changelist",
|
|
||||||
"value": "272006966"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.experiments.source_version",
|
|
||||||
"value": "272166268"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.label",
|
|
||||||
"value": "youtube.ytfe.innertube_20190930_5_RC0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.timestamp",
|
|
||||||
"value": "1569863426"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.variants.checksum",
|
|
||||||
"value": "1a800c1a2396906f1cbb7f670d43b6f5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.run.job",
|
|
||||||
"value": "ytfe-innertube-replica-only.ytfe"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"webResponseContextExtensionData": {
|
|
||||||
"ytConfigData": {
|
|
||||||
"csn": "n2STXd2iKZr2gAOt9qvgCg",
|
|
||||||
"visitorData": "CgtPQm1xTmtvNm1Tcyifyc3sBQ%3D%3D"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"continuationContents": {
|
|
||||||
"liveChatContinuation": {
|
|
||||||
"continuations": [
|
|
||||||
{
|
{
|
||||||
"timedContinuationData": {
|
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
"timeoutMs": 8860,
|
"width": 32,
|
||||||
"continuation": "0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCid_ejQpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DF-ovRpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABtNic0aT75AI%3D"
|
"height": 32
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"addChatItemAction": {
|
|
||||||
"item": {
|
|
||||||
"liveChatPaidMessageRenderer": {
|
|
||||||
"id": "ChwKGkNOblpoTXFrLS1RQ0ZRSU9ZQW9kclhrRXNn",
|
|
||||||
"timestampUsec": "1569940638420061",
|
|
||||||
"authorName": {
|
|
||||||
"simpleText": "九十九 万"
|
|
||||||
},
|
|
||||||
"authorPhoto": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 32,
|
|
||||||
"height": 32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"purchaseAmountText": {
|
|
||||||
"simpleText": "¥846"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"runs": [
|
|
||||||
{
|
|
||||||
"text": "ボルガ博士お許しください代"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"headerBackgroundColor": 4278239141,
|
|
||||||
"headerTextColor": 4278190080,
|
|
||||||
"bodyBackgroundColor": 4280150454,
|
|
||||||
"bodyTextColor": 4278190080,
|
|
||||||
"authorExternalChannelId": "UCoVtCMzg2vleAVsa7kw05AA",
|
|
||||||
"authorNameTextColor": 2315255808,
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"liveChatItemContextMenuEndpoint": {
|
|
||||||
"params": "Q2g0S0hBb2FRMDV1V21oTmNXc3RMVkZEUmxGSlQxbEJiMlJ5V0d0RmMyY1FBQm80Q2cwS0N6Z3dUM3AwU21oMU56YzRLaWNLR0ZWRFNYbDBUbU52ZWpSd1YzcFlaa3hrWVRCRWIxVk1VUklMT0RCUGVuUkthSFUzTnpnZ0FpZ0JNaG9LR0ZWRGIxWjBRMDE2WnpKMmJHVkJWbk5oTjJ0M01EVkJRUSUzRCUzRA=="
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestampColor": 2147483648,
|
|
||||||
"contextMenuAccessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "コメントの操作"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"addLiveChatTickerItemAction": {
|
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
"item": {
|
"width": 64,
|
||||||
"liveChatTickerPaidMessageItemRenderer": {
|
"height": 64
|
||||||
"id": "ChwKGkNOblpoTXFrLS1RQ0ZRSU9ZQW9kclhrRXNn",
|
|
||||||
"amount": {
|
|
||||||
"simpleText": "¥846"
|
|
||||||
},
|
|
||||||
"amountTextColor": 4278190080,
|
|
||||||
"startBackgroundColor": 4280150454,
|
|
||||||
"endBackgroundColor": 4278239141,
|
|
||||||
"authorPhoto": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 32,
|
|
||||||
"height": 32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"durationSec": 120,
|
|
||||||
"showItemEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"showLiveChatItemEndpoint": {
|
|
||||||
"renderer": {
|
|
||||||
"liveChatPaidMessageRenderer": {
|
|
||||||
"id": "ChwKGkNOblpoTXFrLS1RQ0ZRSU9ZQW9kclhrRXNn",
|
|
||||||
"timestampUsec": "1569940638420061",
|
|
||||||
"authorName": {
|
|
||||||
"simpleText": "九十九 万"
|
|
||||||
},
|
|
||||||
"authorPhoto": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 32,
|
|
||||||
"height": 32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"purchaseAmountText": {
|
|
||||||
"simpleText": "¥846"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"runs": [
|
|
||||||
{
|
|
||||||
"text": "ボルガ博士お許しください代"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"headerBackgroundColor": 4278239141,
|
|
||||||
"headerTextColor": 4278190080,
|
|
||||||
"bodyBackgroundColor": 4280150454,
|
|
||||||
"bodyTextColor": 4278190080,
|
|
||||||
"authorExternalChannelId": "UCoVtCMzg2vleAVsa7kw05AA",
|
|
||||||
"authorNameTextColor": 2315255808,
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"liveChatItemContextMenuEndpoint": {
|
|
||||||
"params": "Q2g0S0hBb2FRMDV1V21oTmNXc3RMVkZEUmxGSlQxbEJiMlJ5V0d0RmMyY1FBQm80Q2cwS0N6Z3dUM3AwU21oMU56YzRLaWNLR0ZWRFNYbDBUbU52ZWpSd1YzcFlaa3hrWVRCRWIxVk1VUklMT0RCUGVuUkthSFUzTnpnZ0FpZ0JNaG9LR0ZWRGIxWjBRMDE2WnpKMmJHVkJWbk5oTjJ0M01EVkJRUSUzRCUzRA=="
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestampColor": 2147483648,
|
|
||||||
"contextMenuAccessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "コメントの操作"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"authorExternalChannelId": "UCoVtCMzg2vleAVsa7kw05AA",
|
|
||||||
"fullDurationSec": 120
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"durationSec": "120"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"purchaseAmountText": {
|
||||||
|
"simpleText": "¥846"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "ボルガ博士お許しください代"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"headerBackgroundColor": 4278239141,
|
||||||
|
"headerTextColor": 4278190080,
|
||||||
|
"bodyBackgroundColor": 4280150454,
|
||||||
|
"bodyTextColor": 4278190080,
|
||||||
|
"authorExternalChannelId": "UCoVtCMzg2vleAVsa7kw05AA",
|
||||||
|
"authorNameTextColor": 2315255808,
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"liveChatItemContextMenuEndpoint": {
|
||||||
|
"params": "Q2g0S0hBb2FRMDV1V21oTmNXc3RMVkZEUmxGSlQxbEJiMlJ5V0d0RmMyY1FBQm80Q2cwS0N6Z3dUM3AwU21oMU56YzRLaWNLR0ZWRFNYbDBUbU52ZWpSd1YzcFlaa3hrWVRCRWIxVk1VUklMT0RCUGVuUkthSFUzTnpnZ0FpZ0JNaG9LR0ZWRGIxWjBRMDE2WnpKMmJHVkJWbk5oTjJ0M01EVkJRUSUzRCUzRA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestampColor": 2147483648,
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"addLiveChatTickerItemAction": {
|
||||||
|
"item": {
|
||||||
|
"liveChatTickerPaidMessageItemRenderer": {
|
||||||
|
"id": "ChwKGkNOblpoTXFrLS1RQ0ZRSU9ZQW9kclhrRXNn",
|
||||||
|
"amount": {
|
||||||
|
"simpleText": "¥846"
|
||||||
|
},
|
||||||
|
"amountTextColor": 4278190080,
|
||||||
|
"startBackgroundColor": 4280150454,
|
||||||
|
"endBackgroundColor": 4278239141,
|
||||||
|
"authorPhoto": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
|
"width": 32,
|
||||||
|
"height": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"durationSec": 120,
|
||||||
|
"showItemEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"showLiveChatItemEndpoint": {
|
||||||
|
"renderer": {
|
||||||
|
"liveChatPaidMessageRenderer": {
|
||||||
|
"id": "ChwKGkNOblpoTXFrLS1RQ0ZRSU9ZQW9kclhrRXNn",
|
||||||
|
"timestampUsec": "1569940638420061",
|
||||||
|
"authorName": {
|
||||||
|
"simpleText": "九十九 万"
|
||||||
|
},
|
||||||
|
"authorPhoto": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
|
"width": 32,
|
||||||
|
"height": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"purchaseAmountText": {
|
||||||
|
"simpleText": "¥846"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "ボルガ博士お許しください代"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"headerBackgroundColor": 4278239141,
|
||||||
|
"headerTextColor": 4278190080,
|
||||||
|
"bodyBackgroundColor": 4280150454,
|
||||||
|
"bodyTextColor": 4278190080,
|
||||||
|
"authorExternalChannelId": "UCoVtCMzg2vleAVsa7kw05AA",
|
||||||
|
"authorNameTextColor": 2315255808,
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"liveChatItemContextMenuEndpoint": {
|
||||||
|
"params": "Q2g0S0hBb2FRMDV1V21oTmNXc3RMVkZEUmxGSlQxbEJiMlJ5V0d0RmMyY1FBQm80Q2cwS0N6Z3dUM3AwU21oMU56YzRLaWNLR0ZWRFNYbDBUbU52ZWpSd1YzcFlaa3hrWVRCRWIxVk1VUklMT0RCUGVuUkthSFUzTnpnZ0FpZ0JNaG9LR0ZWRGIxWjBRMDE2WnpKMmJHVkJWbk5oTjJ0M01EVkJRUSUzRCUzRA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestampColor": 2147483648,
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authorExternalChannelId": "UCoVtCMzg2vleAVsa7kw05AA",
|
||||||
|
"fullDurationSec": 120
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"durationSec": "120"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
388
tests/testdata/compatible/supersticker.json
vendored
388
tests/testdata/compatible/supersticker.json
vendored
@@ -1,197 +1,195 @@
|
|||||||
{
|
{
|
||||||
"xsrf_token": "QUFFLUhqbWVqWVRTUjhBcGRkNWR5Q2F3VWhxcTd0RkF1UXxBQ3Jtc0trMThFVGVVNTFnWmZucnkwNlJJMEZ2bndUS1I3b2dpLUtTNE92dEgwX3Y0MkNZU2NXdGY1QTMtX09BUGphYUpDQlc1dFhiTm9jLS1sQXVCNHpsdllqcm1id0t1RFBCM3A1b2o3OGt0Yjd6TE5wcmxBbjNyQktjc1lWZ3hjM1RuYk83YkQ0VVN3MGUybjAwSE90SS1YNkxvMUV5YVE=",
|
"xsrf_token": "QUFFLUhqbWVqWVRTUjhBcGRkNWR5Q2F3VWhxcTd0RkF1UXxBQ3Jtc0trMThFVGVVNTFnWmZucnkwNlJJMEZ2bndUS1I3b2dpLUtTNE92dEgwX3Y0MkNZU2NXdGY1QTMtX09BUGphYUpDQlc1dFhiTm9jLS1sQXVCNHpsdllqcm1id0t1RFBCM3A1b2o3OGt0Yjd6TE5wcmxBbjNyQktjc1lWZ3hjM1RuYk83YkQ0VVN3MGUybjAwSE90SS1YNkxvMUV5YVE=",
|
||||||
"timing": {
|
"timing": {
|
||||||
"info": {
|
"info": {
|
||||||
"st": 148
|
"st": 148
|
||||||
}
|
|
||||||
},
|
|
||||||
"endpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"urlEndpoint": {
|
|
||||||
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"url": "\/live_chat\/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D",
|
|
||||||
"csn": "n2STXd2iKZr2gAOt9qvgCg",
|
|
||||||
"response": {
|
|
||||||
"responseContext": {
|
|
||||||
"serviceTrackingParams": [
|
|
||||||
{
|
|
||||||
"service": "CSI",
|
|
||||||
"params": [
|
|
||||||
{
|
|
||||||
"key": "GetLiveChat_rid",
|
|
||||||
"value": "0x9290108c05344647"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "c",
|
|
||||||
"value": "WEB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "cver",
|
|
||||||
"value": "2.20191001.04.00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "yt_li",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"service": "GFEEDBACK",
|
|
||||||
"params": [
|
|
||||||
{
|
|
||||||
"key": "e",
|
|
||||||
"value": "23744176,23788875,23793834,23794620,23804281,23806159,23816483,23819244,23820768,23826780,23827354,23830392,23832125,23835020,23836965,23837741,23837772,23837993,23838235,23839362,23840155,23840217,23841118,23841454,23842630,23842662,23842883,23842986,23843289,23843534,23843767,23845644,9449243,9471239,9474360"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "logged_in",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"service": "GUIDED_HELP",
|
|
||||||
"params": [
|
|
||||||
{
|
|
||||||
"key": "logged_in",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"service": "ECATCHER",
|
|
||||||
"params": [
|
|
||||||
{
|
|
||||||
"key": "client.name",
|
|
||||||
"value": "WEB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "client.version",
|
|
||||||
"value": "2.20191001"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.changelist",
|
|
||||||
"value": "272006966"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.experiments.source_version",
|
|
||||||
"value": "272166268"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.label",
|
|
||||||
"value": "youtube.ytfe.innertube_20190930_5_RC0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.timestamp",
|
|
||||||
"value": "1569863426"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.build.variants.checksum",
|
|
||||||
"value": "1a800c1a2396906f1cbb7f670d43b6f5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "innertube.run.job",
|
|
||||||
"value": "ytfe-innertube-replica-only.ytfe"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"webResponseContextExtensionData": {
|
|
||||||
"ytConfigData": {
|
|
||||||
"csn": "n2STXd2iKZr2gAOt9qvgCg",
|
|
||||||
"visitorData": "CgtPQm1xTmtvNm1Tcyifyc3sBQ%3D%3D"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"continuationContents": {
|
|
||||||
"liveChatContinuation": {
|
|
||||||
"continuations": [
|
|
||||||
{
|
|
||||||
"timedContinuationData": {
|
|
||||||
"timeoutMs": 8860,
|
|
||||||
"continuation": "0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCid_ejQpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DF-ovRpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABtNic0aT75AI%3D"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"addChatItemAction": {
|
|
||||||
"item": {
|
|
||||||
"liveChatPaidStickerRenderer": {
|
|
||||||
"id": "ChwKGkNQX2Qzb2pUcU9VQ0ZRdnVXQW9kaTNJS3NB",
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"liveChatItemContextMenuEndpoint": {
|
|
||||||
"params": "Q2g0S0hBb2FRMUJmWkROdmFsUnhUMVZEUmxGMmRWZEJiMlJwTTBsTGMwRVFBQm80Q2cwS0N6VlVUSE42U0hNd2QxYzBLaWNLR0ZWRFdGSnNTVXN6UTNkZlZFcEpVVU0xYTFOS1NsRk5aeElMTlZSTWMzcEljekIzVnpRZ0FpZ0JNaG9LR0ZWRFRHOXJPVWQ0WVRGYU5rWTVWV3d5WVV0MlRFWkdadyUzRCUzRA=="
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contextMenuAccessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "コメントの操作"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestampUsec": "1571499325098699",
|
|
||||||
"authorPhoto": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "https: //yt3.ggpht.com/-xRQVNtDSO3w/AAAAAAAAAAI/AAAAAAAAAAA/Is9D9D7wwAE/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 32,
|
|
||||||
"height": 32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-xRQVNtDSO3w/AAAAAAAAAAI/AAAAAAAAAAA/Is9D9D7wwAE/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"authorName": {
|
|
||||||
"simpleText": "りお"
|
|
||||||
},
|
|
||||||
"authorExternalChannelId": "UCLok9Gxa1Z6F9Ul2aKvLFFg",
|
|
||||||
"sticker": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "//lh3.googleusercontent.com/1aIk6vlk4gZ2ytc42j3WcIHYtWFWo2uVWVqbHFuxiGHO4XwyAS0u8vuu6VkiX5eR6uy9mfAupyP786_TbP0=s72-rwa",
|
|
||||||
"width": 72,
|
|
||||||
"height": 72
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "//lh3.googleusercontent.com/1aIk6vlk4gZ2ytc42j3WcIHYtWFWo2uVWVqbHFuxiGHO4XwyAS0u8vuu6VkiX5eR6uy9mfAupyP786_TbP0=s144-rwa",
|
|
||||||
"width": 144,
|
|
||||||
"height": 144
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"accessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "気付いてもらえるように人差し指を上げたり下げたりしている柴犬"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"moneyChipBackgroundColor": 4278248959,
|
|
||||||
"moneyChipTextColor": 4278190080,
|
|
||||||
"purchaseAmountText": {
|
|
||||||
"simpleText": "¥200"
|
|
||||||
},
|
|
||||||
"stickerDisplayWidth": 72,
|
|
||||||
"stickerDisplayHeight": 72,
|
|
||||||
"backgroundColor": 4278237396,
|
|
||||||
"authorNameTextColor": 3003121664
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"urlEndpoint": {
|
||||||
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D",
|
||||||
|
"csn": "n2STXd2iKZr2gAOt9qvgCg",
|
||||||
|
"responseContext": {
|
||||||
|
"serviceTrackingParams": [
|
||||||
|
{
|
||||||
|
"service": "CSI",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "GetLiveChat_rid",
|
||||||
|
"value": "0x9290108c05344647"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "c",
|
||||||
|
"value": "WEB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "cver",
|
||||||
|
"value": "2.20191001.04.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "yt_li",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "GFEEDBACK",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "e",
|
||||||
|
"value": "23744176,23788875,23793834,23794620,23804281,23806159,23816483,23819244,23820768,23826780,23827354,23830392,23832125,23835020,23836965,23837741,23837772,23837993,23838235,23839362,23840155,23840217,23841118,23841454,23842630,23842662,23842883,23842986,23843289,23843534,23843767,23845644,9449243,9471239,9474360"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "logged_in",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "GUIDED_HELP",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "logged_in",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "ECATCHER",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"key": "client.name",
|
||||||
|
"value": "WEB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "client.version",
|
||||||
|
"value": "2.20191001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.changelist",
|
||||||
|
"value": "272006966"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.experiments.source_version",
|
||||||
|
"value": "272166268"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.label",
|
||||||
|
"value": "youtube.ytfe.innertube_20190930_5_RC0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.timestamp",
|
||||||
|
"value": "1569863426"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.build.variants.checksum",
|
||||||
|
"value": "1a800c1a2396906f1cbb7f670d43b6f5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "innertube.run.job",
|
||||||
|
"value": "ytfe-innertube-replica-only.ytfe"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"webResponseContextExtensionData": {
|
||||||
|
"ytConfigData": {
|
||||||
|
"csn": "n2STXd2iKZr2gAOt9qvgCg",
|
||||||
|
"visitorData": "CgtPQm1xTmtvNm1Tcyifyc3sBQ%3D%3D"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"continuationContents": {
|
||||||
|
"liveChatContinuation": {
|
||||||
|
"continuations": [
|
||||||
|
{
|
||||||
|
"timedContinuationData": {
|
||||||
|
"timeoutMs": 8860,
|
||||||
|
"continuation": "0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCid_ejQpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DF-ovRpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABtNic0aT75AI%3D"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"addChatItemAction": {
|
||||||
|
"item": {
|
||||||
|
"liveChatPaidStickerRenderer": {
|
||||||
|
"id": "ChwKGkNQX2Qzb2pUcU9VQ0ZRdnVXQW9kaTNJS3NB",
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"liveChatItemContextMenuEndpoint": {
|
||||||
|
"params": "Q2g0S0hBb2FRMUJmWkROdmFsUnhUMVZEUmxGMmRWZEJiMlJwTTBsTGMwRVFBQm80Q2cwS0N6VlVUSE42U0hNd2QxYzBLaWNLR0ZWRFdGSnNTVXN6UTNkZlZFcEpVVU0xYTFOS1NsRk5aeElMTlZSTWMzcEljekIzVnpRZ0FpZ0JNaG9LR0ZWRFRHOXJPVWQ0WVRGYU5rWTVWV3d5WVV0MlRFWkdadyUzRCUzRA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestampUsec": "1571499325098699",
|
||||||
|
"authorPhoto": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https: //yt3.ggpht.com/-xRQVNtDSO3w/AAAAAAAAAAI/AAAAAAAAAAA/Is9D9D7wwAE/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
|
"width": 32,
|
||||||
|
"height": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/-xRQVNtDSO3w/AAAAAAAAAAI/AAAAAAAAAAA/Is9D9D7wwAE/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"authorName": {
|
||||||
|
"simpleText": "りお"
|
||||||
|
},
|
||||||
|
"authorExternalChannelId": "UCLok9Gxa1Z6F9Ul2aKvLFFg",
|
||||||
|
"sticker": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "//lh3.googleusercontent.com/1aIk6vlk4gZ2ytc42j3WcIHYtWFWo2uVWVqbHFuxiGHO4XwyAS0u8vuu6VkiX5eR6uy9mfAupyP786_TbP0=s72-rwa",
|
||||||
|
"width": 72,
|
||||||
|
"height": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "//lh3.googleusercontent.com/1aIk6vlk4gZ2ytc42j3WcIHYtWFWo2uVWVqbHFuxiGHO4XwyAS0u8vuu6VkiX5eR6uy9mfAupyP786_TbP0=s144-rwa",
|
||||||
|
"width": 144,
|
||||||
|
"height": 144
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "気付いてもらえるように人差し指を上げたり下げたりしている柴犬"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moneyChipBackgroundColor": 4278248959,
|
||||||
|
"moneyChipTextColor": 4278190080,
|
||||||
|
"purchaseAmountText": {
|
||||||
|
"simpleText": "¥200"
|
||||||
|
},
|
||||||
|
"stickerDisplayWidth": 72,
|
||||||
|
"stickerDisplayHeight": 72,
|
||||||
|
"backgroundColor": 4278237396,
|
||||||
|
"authorNameTextColor": 3003121664
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
348
tests/testdata/compatible/textmessage.json
vendored
348
tests/testdata/compatible/textmessage.json
vendored
@@ -1,177 +1,175 @@
|
|||||||
{
|
{
|
||||||
"response": {
|
"responseContext": {
|
||||||
"responseContext": {
|
"serviceTrackingParams": [
|
||||||
"serviceTrackingParams": [
|
{
|
||||||
{
|
"service": "CSI",
|
||||||
"service": "CSI",
|
"params": [
|
||||||
"params": [
|
{
|
||||||
{
|
"key": "GetLiveChat_rid",
|
||||||
"key": "GetLiveChat_rid",
|
"value": "0x3eff0db28fc39bbe"
|
||||||
"value": "0x3eff0db28fc39bbe"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "c",
|
||||||
"key": "c",
|
"value": "WEB"
|
||||||
"value": "WEB"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "cver",
|
||||||
"key": "cver",
|
"value": "2.20190920.05.01"
|
||||||
"value": "2.20190920.05.01"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "yt_li",
|
||||||
"key": "yt_li",
|
"value": "0"
|
||||||
"value": "0"
|
}
|
||||||
}
|
]
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
"service": "GFEEDBACK",
|
||||||
"service": "GFEEDBACK",
|
"params": [
|
||||||
"params": [
|
{
|
||||||
{
|
"key": "e",
|
||||||
"key": "e",
|
"value": "23744176,23748146,23788851,23788875,23793834,23804281,23807353,23808952,23828082,23828243,23829333,23832544,23834418,23834656,23835020,23836434,23836965,23837742,23837772,23837993,23838301,23838576,23838576,23838742,23839360,23840216,23841655,23842986,23843288,23843533,23843743,23844780,24630231,9425362,9449243,9466592,9469037,9471235,9474358"
|
||||||
"value": "23744176,23748146,23788851,23788875,23793834,23804281,23807353,23808952,23828082,23828243,23829333,23832544,23834418,23834656,23835020,23836434,23836965,23837742,23837772,23837993,23838301,23838576,23838576,23838742,23839360,23840216,23841655,23842986,23843288,23843533,23843743,23844780,24630231,9425362,9449243,9466592,9469037,9471235,9474358"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "logged_in",
|
||||||
"key": "logged_in",
|
"value": "0"
|
||||||
"value": "0"
|
}
|
||||||
}
|
]
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
"service": "GUIDED_HELP",
|
||||||
"service": "GUIDED_HELP",
|
"params": [
|
||||||
"params": [
|
{
|
||||||
{
|
"key": "logged_in",
|
||||||
"key": "logged_in",
|
"value": "0"
|
||||||
"value": "0"
|
}
|
||||||
}
|
]
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
"service": "ECATCHER",
|
||||||
"service": "ECATCHER",
|
"params": [
|
||||||
"params": [
|
{
|
||||||
{
|
"key": "client.name",
|
||||||
"key": "client.name",
|
"value": "WEB"
|
||||||
"value": "WEB"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "client.version",
|
||||||
"key": "client.version",
|
"value": "2.20190920"
|
||||||
"value": "2.20190920"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "innertube.build.changelist",
|
||||||
"key": "innertube.build.changelist",
|
"value": "270169303"
|
||||||
"value": "270169303"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "innertube.build.experiments.source_version",
|
||||||
"key": "innertube.build.experiments.source_version",
|
"value": "270377311"
|
||||||
"value": "270377311"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "innertube.build.label",
|
||||||
"key": "innertube.build.label",
|
"value": "youtube.ytfe.innertube_20190919_5_RC1"
|
||||||
"value": "youtube.ytfe.innertube_20190919_5_RC1"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "innertube.build.timestamp",
|
||||||
"key": "innertube.build.timestamp",
|
"value": "1568942548"
|
||||||
"value": "1568942548"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "innertube.build.variants.checksum",
|
||||||
"key": "innertube.build.variants.checksum",
|
"value": "392d499f55b5e2c240adde58886a8143"
|
||||||
"value": "392d499f55b5e2c240adde58886a8143"
|
},
|
||||||
},
|
{
|
||||||
{
|
"key": "innertube.run.job",
|
||||||
"key": "innertube.run.job",
|
"value": "ytfe-innertube-replica-only.ytfe"
|
||||||
"value": "ytfe-innertube-replica-only.ytfe"
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
}
|
],
|
||||||
],
|
"webResponseContextExtensionData": {
|
||||||
"webResponseContextExtensionData": {
|
"ytConfigData": {
|
||||||
"ytConfigData": {
|
"csn": "n96GXabRGouFlQTigY2YDg",
|
||||||
"csn": "n96GXabRGouFlQTigY2YDg",
|
"visitorData": "CgtKUldQeGJJRXhkcyifvZvsBQ%3D%3D"
|
||||||
"visitorData": "CgtKUldQeGJJRXhkcyifvZvsBQ%3D%3D"
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"continuationContents": {
|
|
||||||
"liveChatContinuation": {
|
|
||||||
"continuations": [
|
|
||||||
{
|
|
||||||
"timedContinuationData": {
|
|
||||||
"timeoutMs": 5041,
|
|
||||||
"continuation": "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCj7hLmSs-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1DsuuGSs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKAB7KDVk7Pj5AI%3D"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"addChatItemAction": {
|
|
||||||
"item": {
|
|
||||||
"liveChatTextMessageRenderer": {
|
|
||||||
"message": {
|
|
||||||
"runs": [
|
|
||||||
{
|
|
||||||
"text": "text"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"authorName": {
|
|
||||||
"simpleText": "name"
|
|
||||||
},
|
|
||||||
"authorPhoto": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-8sLtPu1Hyw0/AAAAAAAAAAI/AAAAAAAAAAA/a_52bWnC0-s/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 32,
|
|
||||||
"height": 32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/-8sLtPu1Hyw0/AAAAAAAAAAI/AAAAAAAAAAA/a_52bWnC0-s/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"liveChatItemContextMenuEndpoint": {
|
|
||||||
"params": "Q2pzS09Rb2FRMHRQTkhRMVEzbzBMVkZEUmxweGRrUlJiMlJZV0d0QlVHY1NHME5PYm5OdlVESm1OQzFSUTBaUmRsTlhRVzlrVVZGclJUTlJNeEFBR2pnS0RRb0xhelJMZDBsWFQwdERkalFxSndvWVZVTnZTWEZ1TVZvMWFYaERXbmRqTUVSWFNqZHlTME5uRWd0ck5FdDNTVmRQUzBOMk5DQUNLQUV5R2dvWVZVTnlOVGxXVlY5amRtWnlkVEF0YW1GeWNtUk1NMDEz"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "CjkKGkNLTzR0NUN6NC1RQ0ZacXZEUW9kWFhrQVBnEhtDTm5zb1AyZjQtUUNGUXZTV0FvZFFRa0UzUTM%3D",
|
|
||||||
"timestampUsec": "1569119896722467",
|
|
||||||
"authorExternalChannelId": "UCr59VU_cvfru0-jarrdL3Mw",
|
|
||||||
"contextMenuAccessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "コメントの操作"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"clientId": "CNnsoP2f4-QCFQvSWAodQQkE3Q3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"endpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%253D"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"urlEndpoint": {
|
|
||||||
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%253D"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"csn": "n96GXabRGouFlQTigY2YDg",
|
|
||||||
"xsrf_token": "QUFFLUhqbHNNWTF3NFJqc2h3cGE1NE9FWGdaWk5mRlVhUXxBQ3Jtc0tuTWhZNFcyTW1iZnA3ZnFTYUFudVFEUVE0cnFEOVBGcEU1MEh0Zlh4bll1amVmRl9OMkxZV3pKV1ZSbExBeDctTl95NGtBVnJZdlNxeS1KdWVNempEN2N6MHhaU1laV3hnVkZPeHp1OHVDTGVFSGUyOGduT0szbDV5N05LYUZTdzdoTDRwV1VJWndaVjdQVGRjNWVpR0YwUXgtZXc=",
|
|
||||||
"url": "\/live_chat\/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%253D",
|
|
||||||
"timing": {
|
|
||||||
"info": {
|
|
||||||
"st": 81
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"continuationContents": {
|
||||||
|
"liveChatContinuation": {
|
||||||
|
"continuations": [
|
||||||
|
{
|
||||||
|
"timedContinuationData": {
|
||||||
|
"timeoutMs": 5041,
|
||||||
|
"continuation": "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCj7hLmSs-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1DsuuGSs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKAB7KDVk7Pj5AI%3D"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"addChatItemAction": {
|
||||||
|
"item": {
|
||||||
|
"liveChatTextMessageRenderer": {
|
||||||
|
"message": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"authorName": {
|
||||||
|
"simpleText": "name"
|
||||||
|
},
|
||||||
|
"authorPhoto": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/-8sLtPu1Hyw0/AAAAAAAAAAI/AAAAAAAAAAA/a_52bWnC0-s/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
|
"width": 32,
|
||||||
|
"height": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/-8sLtPu1Hyw0/AAAAAAAAAAI/AAAAAAAAAAA/a_52bWnC0-s/s64-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"liveChatItemContextMenuEndpoint": {
|
||||||
|
"params": "Q2pzS09Rb2FRMHRQTkhRMVEzbzBMVkZEUmxweGRrUlJiMlJZV0d0QlVHY1NHME5PYm5OdlVESm1OQzFSUTBaUmRsTlhRVzlrVVZGclJUTlJNeEFBR2pnS0RRb0xhelJMZDBsWFQwdERkalFxSndvWVZVTnZTWEZ1TVZvMWFYaERXbmRqTUVSWFNqZHlTME5uRWd0ck5FdDNTVmRQUzBOMk5DQUNLQUV5R2dvWVZVTnlOVGxXVlY5amRtWnlkVEF0YW1GeWNtUk1NMDEz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "CjkKGkNLTzR0NUN6NC1RQ0ZacXZEUW9kWFhrQVBnEhtDTm5zb1AyZjQtUUNGUXZTV0FvZFFRa0UzUTM%3D",
|
||||||
|
"timestampUsec": "1569119896722467",
|
||||||
|
"authorExternalChannelId": "UCr59VU_cvfru0-jarrdL3Mw",
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clientId": "CNnsoP2f4-QCFQvSWAodQQkE3Q3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%253D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"urlEndpoint": {
|
||||||
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%253D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"csn": "n96GXabRGouFlQTigY2YDg",
|
||||||
|
"xsrf_token": "QUFFLUhqbHNNWTF3NFJqc2h3cGE1NE9FWGdaWk5mRlVhUXxBQ3Jtc0tuTWhZNFcyTW1iZnA3ZnFTYUFudVFEUVE0cnFEOVBGcEU1MEh0Zlh4bll1amVmRl9OMkxZV3pKV1ZSbExBeDctTl95NGtBVnJZdlNxeS1KdWVNempEN2N6MHhaU1laV3hnVkZPeHp1OHVDTGVFSGUyOGduT0szbDV5N05LYUZTdzdoTDRwV1VJWndaVjdQVGRjNWVpR0YwUXgtZXc=",
|
||||||
|
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%253D",
|
||||||
|
"timing": {
|
||||||
|
"info": {
|
||||||
|
"st": 81
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
166
tests/testdata/default/newSponsor_current.json
vendored
166
tests/testdata/default/newSponsor_current.json
vendored
@@ -1,100 +1,98 @@
|
|||||||
{
|
{
|
||||||
"response": {
|
"responseContext": {
|
||||||
"responseContext": {
|
"webResponseContextExtensionData": ""
|
||||||
"webResponseContextExtensionData": ""
|
},
|
||||||
},
|
"continuationContents": {
|
||||||
"continuationContents": {
|
"liveChatContinuation": {
|
||||||
"liveChatContinuation": {
|
"continuations": [
|
||||||
"continuations": [
|
{
|
||||||
{
|
"invalidationContinuationData": {
|
||||||
"invalidationContinuationData": {
|
"invalidationId": {
|
||||||
"invalidationId": {
|
"objectSource": 1000,
|
||||||
"objectSource": 1000,
|
"objectId": "___objectId___",
|
||||||
"objectId": "___objectId___",
|
"topic": "chat~00000000000~0000000",
|
||||||
"topic": "chat~00000000000~0000000",
|
"subscribeToGcmTopics": true,
|
||||||
"subscribeToGcmTopics": true,
|
"protoCreationTimestampMs": "1577804400000"
|
||||||
"protoCreationTimestampMs": "1577804400000"
|
},
|
||||||
},
|
"timeoutMs": 10000,
|
||||||
"timeoutMs": 10000,
|
"continuation": "___continuation___"
|
||||||
"continuation": "___continuation___"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"actions": [
|
],
|
||||||
{
|
"actions": [
|
||||||
"addChatItemAction": {
|
{
|
||||||
"item": {
|
"addChatItemAction": {
|
||||||
"liveChatMembershipItemRenderer": {
|
"item": {
|
||||||
"id": "dummy_id",
|
"liveChatMembershipItemRenderer": {
|
||||||
"timestampUsec": 1570678496000000,
|
"id": "dummy_id",
|
||||||
"authorExternalChannelId": "author_channel_id",
|
"timestampUsec": 1570678496000000,
|
||||||
"headerSubtext": {
|
"authorExternalChannelId": "author_channel_id",
|
||||||
"runs": [
|
"headerSubtext": {
|
||||||
{
|
"runs": [
|
||||||
"text": "新規メンバー"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"authorName": {
|
|
||||||
"simpleText": "author_name"
|
|
||||||
},
|
|
||||||
"authorPhoto": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 32,
|
|
||||||
"height": 32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"authorBadges": [
|
|
||||||
{
|
{
|
||||||
"liveChatAuthorBadgeRenderer": {
|
"text": "新規メンバー"
|
||||||
"customThumbnail": {
|
}
|
||||||
"thumbnails": [
|
]
|
||||||
{
|
},
|
||||||
"url": "https://yt3.ggpht.com/X=s32-c-k"
|
"authorName": {
|
||||||
},
|
"simpleText": "author_name"
|
||||||
{
|
},
|
||||||
"url": "https://yt3.ggpht.com/X=s64-c-k"
|
"authorPhoto": {
|
||||||
}
|
"thumbnails": [
|
||||||
]
|
{
|
||||||
},
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
"tooltip": "新規メンバー",
|
"width": 32,
|
||||||
"accessibility": {
|
"height": 32
|
||||||
"accessibilityData": {
|
},
|
||||||
"label": "新規メンバー"
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"authorBadges": [
|
||||||
|
{
|
||||||
|
"liveChatAuthorBadgeRenderer": {
|
||||||
|
"customThumbnail": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/X=s32-c-k"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/X=s64-c-k"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tooltip": "新規メンバー",
|
||||||
|
"accessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "新規メンバー"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"contextMenuEndpoint": {
|
],
|
||||||
"commandMetadata": {
|
"contextMenuEndpoint": {
|
||||||
"webCommandMetadata": {
|
"commandMetadata": {
|
||||||
"ignoreNavigation": true
|
"webCommandMetadata": {
|
||||||
}
|
"ignoreNavigation": true
|
||||||
},
|
|
||||||
"liveChatItemContextMenuEndpoint": {
|
|
||||||
"params": "___params___"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contextMenuAccessibility": {
|
"liveChatItemContextMenuEndpoint": {
|
||||||
"accessibilityData": {
|
"params": "___params___"
|
||||||
"label": "コメントの操作"
|
}
|
||||||
}
|
},
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
tests/testdata/default/newSponsor_lagacy.json
vendored
138
tests/testdata/default/newSponsor_lagacy.json
vendored
@@ -1,82 +1,80 @@
|
|||||||
{
|
{
|
||||||
"response": {
|
"responseContext": {
|
||||||
"responseContext": {
|
"webResponseContextExtensionData": ""
|
||||||
"webResponseContextExtensionData": ""
|
},
|
||||||
},
|
"continuationContents": {
|
||||||
"continuationContents": {
|
"liveChatContinuation": {
|
||||||
"liveChatContinuation": {
|
"continuations": [
|
||||||
"continuations": [
|
{
|
||||||
{
|
"invalidationContinuationData": {
|
||||||
"invalidationContinuationData": {
|
"invalidationId": {
|
||||||
"invalidationId": {
|
"objectSource": 1000,
|
||||||
"objectSource": 1000,
|
"objectId": "___objectId___",
|
||||||
"objectId": "___objectId___",
|
"topic": "chat~00000000000~0000000",
|
||||||
"topic": "chat~00000000000~0000000",
|
"subscribeToGcmTopics": true,
|
||||||
"subscribeToGcmTopics": true,
|
"protoCreationTimestampMs": "1577804400000"
|
||||||
"protoCreationTimestampMs": "1577804400000"
|
},
|
||||||
},
|
"timeoutMs": 10000,
|
||||||
"timeoutMs": 10000,
|
"continuation": "___continuation___"
|
||||||
"continuation": "___continuation___"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"actions": [
|
],
|
||||||
{
|
"actions": [
|
||||||
"addChatItemAction": {
|
{
|
||||||
"item": {
|
"addChatItemAction": {
|
||||||
"liveChatLegacyPaidMessageRenderer": {
|
"item": {
|
||||||
"id": "dummy_id",
|
"liveChatLegacyPaidMessageRenderer": {
|
||||||
"timestampUsec": 1570678496000000,
|
"id": "dummy_id",
|
||||||
"eventText": {
|
"timestampUsec": 1570678496000000,
|
||||||
"runs": [
|
"eventText": {
|
||||||
{
|
"runs": [
|
||||||
"text": "新規メンバー"
|
{
|
||||||
}
|
"text": "新規メンバー"
|
||||||
]
|
}
|
||||||
},
|
]
|
||||||
"detailText": {
|
},
|
||||||
"simpleText": "ようこそ、author_name!"
|
"detailText": {
|
||||||
},
|
"simpleText": "ようこそ、author_name!"
|
||||||
"authorName": {
|
},
|
||||||
"simpleText": "author_name"
|
"authorName": {
|
||||||
},
|
"simpleText": "author_name"
|
||||||
"authorPhoto": {
|
},
|
||||||
"thumbnails": [
|
"authorPhoto": {
|
||||||
{
|
"thumbnails": [
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
{
|
||||||
"width": 32,
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
"height": 32
|
"width": 32,
|
||||||
},
|
"height": 32
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"authorExternalChannelId": "author_channel_id",
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"clickTrackingParams": "___clickTrackingParams___",
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"liveChatItemContextMenuEndpoint": {
|
{
|
||||||
"params": "___params___"
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"authorExternalChannelId": "author_channel_id",
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"clickTrackingParams": "___clickTrackingParams___",
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contextMenuAccessibility": {
|
"liveChatItemContextMenuEndpoint": {
|
||||||
"accessibilityData": {
|
"params": "___params___"
|
||||||
"label": "コメントの操作"
|
}
|
||||||
}
|
},
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
192
tests/testdata/default/replay_member_text.json
vendored
192
tests/testdata/default/replay_member_text.json
vendored
@@ -1,112 +1,110 @@
|
|||||||
{
|
{
|
||||||
"response": {
|
"responseContext": {
|
||||||
"responseContext": {
|
"webResponseContextExtensionData": "data"
|
||||||
"webResponseContextExtensionData": "data"
|
},
|
||||||
},
|
"continuationContents": {
|
||||||
"continuationContents": {
|
"liveChatContinuation": {
|
||||||
"liveChatContinuation": {
|
"continuations": [
|
||||||
"continuations": [
|
{
|
||||||
{
|
"liveChatReplayContinuationData": {
|
||||||
"liveChatReplayContinuationData": {
|
"invalidationId": {
|
||||||
"invalidationId": {
|
"objectSource": 1000,
|
||||||
"objectSource": 1000,
|
"objectId": "___objectId___",
|
||||||
"objectId": "___objectId___",
|
"topic": "chat~00000000000~0000000",
|
||||||
"topic": "chat~00000000000~0000000",
|
"subscribeToGcmTopics": true,
|
||||||
"subscribeToGcmTopics": true,
|
"protoCreationTimestampMs": "1577804400000"
|
||||||
"protoCreationTimestampMs": "1577804400000"
|
},
|
||||||
},
|
"timeoutMs": 10000,
|
||||||
"timeoutMs": 10000,
|
"continuation": "___continuation___"
|
||||||
"continuation": "___continuation___"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"actions": [
|
],
|
||||||
{
|
"actions": [
|
||||||
"replayChatItemAction": {
|
{
|
||||||
"actions": [
|
"replayChatItemAction": {
|
||||||
{
|
"actions": [
|
||||||
"addChatItemAction": {
|
{
|
||||||
"item": {
|
"addChatItemAction": {
|
||||||
"liveChatTextMessageRenderer": {
|
"item": {
|
||||||
"message": {
|
"liveChatTextMessageRenderer": {
|
||||||
"runs": [
|
"message": {
|
||||||
{
|
"runs": [
|
||||||
"text": "dummy_message"
|
{
|
||||||
}
|
"text": "dummy_message"
|
||||||
]
|
}
|
||||||
},
|
]
|
||||||
"authorName": {
|
},
|
||||||
"simpleText": "author_name"
|
"authorName": {
|
||||||
},
|
"simpleText": "author_name"
|
||||||
"authorPhoto": {
|
},
|
||||||
"thumbnails": [
|
"authorPhoto": {
|
||||||
{
|
"thumbnails": [
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
{
|
||||||
"width": 32,
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
"height": 32
|
"width": 32,
|
||||||
},
|
"height": 32
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"clickTrackingParams": "___clickTrackingParams___",
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"liveChatItemContextMenuEndpoint": {
|
{
|
||||||
"params": "___params___"
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"clickTrackingParams": "___clickTrackingParams___",
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "dummy_id",
|
"liveChatItemContextMenuEndpoint": {
|
||||||
"timestampUsec": 1570678496000000,
|
"params": "___params___"
|
||||||
"authorBadges": [
|
}
|
||||||
{
|
},
|
||||||
"liveChatAuthorBadgeRenderer": {
|
"id": "dummy_id",
|
||||||
"customThumbnail": {
|
"timestampUsec": 1570678496000000,
|
||||||
"thumbnails": [
|
"authorBadges": [
|
||||||
{
|
{
|
||||||
"url": "https://yt3.ggpht.com/X=s16-c-k"
|
"liveChatAuthorBadgeRenderer": {
|
||||||
},
|
"customThumbnail": {
|
||||||
{
|
"thumbnails": [
|
||||||
"url": "https://yt3.ggpht.com/X=s32-c-k"
|
{
|
||||||
}
|
"url": "https://yt3.ggpht.com/X=s16-c-k"
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
"tooltip": "メンバー(1 か月)",
|
"url": "https://yt3.ggpht.com/X=s32-c-k"
|
||||||
"accessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "メンバー(1 か月)"
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tooltip": "メンバー(1 か月)",
|
||||||
|
"accessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "メンバー(1 か月)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"authorExternalChannelId": "author_channel_id",
|
|
||||||
"contextMenuAccessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "コメントの操作"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestampText": {
|
|
||||||
"simpleText": "1:23:45"
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"authorExternalChannelId": "author_channel_id",
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestampText": {
|
||||||
|
"simpleText": "1:23:45"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"clientId": "dummy_client_id"
|
},
|
||||||
}
|
"clientId": "dummy_client_id"
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"videoOffsetTimeMsec": "5025120"
|
],
|
||||||
}
|
"videoOffsetTimeMsec": "5025120"
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
326
tests/testdata/default/superchat.json
vendored
326
tests/testdata/default/superchat.json
vendored
@@ -1,184 +1,182 @@
|
|||||||
{
|
{
|
||||||
"response": {
|
"responseContext": {
|
||||||
"responseContext": {
|
"webResponseContextExtensionData": ""
|
||||||
"webResponseContextExtensionData": ""
|
},
|
||||||
},
|
"continuationContents": {
|
||||||
"continuationContents": {
|
"liveChatContinuation": {
|
||||||
"liveChatContinuation": {
|
"continuations": [
|
||||||
"continuations": [
|
{
|
||||||
{
|
"invalidationContinuationData": {
|
||||||
"invalidationContinuationData": {
|
"invalidationId": {
|
||||||
"invalidationId": {
|
"objectSource": 1000,
|
||||||
"objectSource": 1000,
|
"objectId": "___objectId___",
|
||||||
"objectId": "___objectId___",
|
"topic": "chat~00000000000~0000000",
|
||||||
"topic": "chat~00000000000~0000000",
|
"subscribeToGcmTopics": true,
|
||||||
"subscribeToGcmTopics": true,
|
"protoCreationTimestampMs": "1577804400000"
|
||||||
"protoCreationTimestampMs": "1577804400000"
|
},
|
||||||
},
|
"timeoutMs": 10000,
|
||||||
"timeoutMs": 10000,
|
"continuation": "___continuation___"
|
||||||
"continuation": "___continuation___"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"actions": [
|
],
|
||||||
{
|
"actions": [
|
||||||
"addChatItemAction": {
|
{
|
||||||
"item": {
|
"addChatItemAction": {
|
||||||
"liveChatPaidMessageRenderer": {
|
"item": {
|
||||||
"id": "dummy_id",
|
"liveChatPaidMessageRenderer": {
|
||||||
"timestampUsec": 1570678496000000,
|
"id": "dummy_id",
|
||||||
"authorName": {
|
"timestampUsec": 1570678496000000,
|
||||||
"simpleText": "author_name"
|
"authorName": {
|
||||||
},
|
"simpleText": "author_name"
|
||||||
"authorPhoto": {
|
},
|
||||||
"thumbnails": [
|
"authorPhoto": {
|
||||||
{
|
"thumbnails": [
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
{
|
||||||
"width": 32,
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
"height": 32
|
"width": 32,
|
||||||
},
|
"height": 32
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"purchaseAmountText": {
|
|
||||||
"simpleText": "¥800"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"runs": [
|
|
||||||
{
|
|
||||||
"text": "dummy_message"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"headerBackgroundColor": 4278239141,
|
|
||||||
"headerTextColor": 4278190080,
|
|
||||||
"bodyBackgroundColor": 4280150454,
|
|
||||||
"bodyTextColor": 4278190080,
|
|
||||||
"authorExternalChannelId": "author_channel_id",
|
|
||||||
"authorNameTextColor": 2315255808,
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"liveChatItemContextMenuEndpoint": {
|
{
|
||||||
"params": "___params___"
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"purchaseAmountText": {
|
||||||
|
"simpleText": "¥800"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "dummy_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"headerBackgroundColor": 4278239141,
|
||||||
|
"headerTextColor": 4278190080,
|
||||||
|
"bodyBackgroundColor": 4280150454,
|
||||||
|
"bodyTextColor": 4278190080,
|
||||||
|
"authorExternalChannelId": "author_channel_id",
|
||||||
|
"authorNameTextColor": 2315255808,
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timestampColor": 2147483648,
|
"liveChatItemContextMenuEndpoint": {
|
||||||
"contextMenuAccessibility": {
|
"params": "___params___"
|
||||||
"accessibilityData": {
|
}
|
||||||
"label": "コメントの操作"
|
},
|
||||||
}
|
"timestampColor": 2147483648,
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{
|
},
|
||||||
"addLiveChatTickerItemAction": {
|
{
|
||||||
"item": {
|
"addLiveChatTickerItemAction": {
|
||||||
"liveChatTickerPaidMessageItemRenderer": {
|
"item": {
|
||||||
"id": "dummy_id",
|
"liveChatTickerPaidMessageItemRenderer": {
|
||||||
"amount": {
|
"id": "dummy_id",
|
||||||
"simpleText": "¥846"
|
"amount": {
|
||||||
},
|
"simpleText": "¥846"
|
||||||
"amountTextColor": 4278190080,
|
},
|
||||||
"startBackgroundColor": 4280150454,
|
"amountTextColor": 4278190080,
|
||||||
"endBackgroundColor": 4278239141,
|
"startBackgroundColor": 4280150454,
|
||||||
"authorPhoto": {
|
"endBackgroundColor": 4278239141,
|
||||||
"thumbnails": [
|
"authorPhoto": {
|
||||||
{
|
"thumbnails": [
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
{
|
||||||
"width": 32,
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
"height": 32
|
"width": 32,
|
||||||
},
|
"height": 32
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"durationSec": 120,
|
|
||||||
"showItemEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"showLiveChatItemEndpoint": {
|
{
|
||||||
"renderer": {
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
"liveChatPaidMessageRenderer": {
|
"width": 64,
|
||||||
"id": "dummy_id",
|
"height": 64
|
||||||
"timestampUsec": 1570678496000000,
|
}
|
||||||
"authorName": {
|
]
|
||||||
"simpleText": "author_name"
|
},
|
||||||
},
|
"durationSec": 120,
|
||||||
"authorPhoto": {
|
"showItemEndpoint": {
|
||||||
"thumbnails": [
|
"commandMetadata": {
|
||||||
{
|
"webCommandMetadata": {
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
"ignoreNavigation": true
|
||||||
"width": 32,
|
}
|
||||||
"height": 32
|
},
|
||||||
},
|
"showLiveChatItemEndpoint": {
|
||||||
{
|
"renderer": {
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
"liveChatPaidMessageRenderer": {
|
||||||
"width": 64,
|
"id": "dummy_id",
|
||||||
"height": 64
|
"timestampUsec": 1570678496000000,
|
||||||
}
|
"authorName": {
|
||||||
]
|
"simpleText": "author_name"
|
||||||
},
|
},
|
||||||
"purchaseAmountText": {
|
"authorPhoto": {
|
||||||
"simpleText": "¥846"
|
"thumbnails": [
|
||||||
},
|
{
|
||||||
"message": {
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
"runs": [
|
"width": 32,
|
||||||
{
|
"height": 32
|
||||||
"text": "dummy_message"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"headerBackgroundColor": 4278239141,
|
|
||||||
"headerTextColor": 4278190080,
|
|
||||||
"bodyBackgroundColor": 4280150454,
|
|
||||||
"bodyTextColor": 4278190080,
|
|
||||||
"authorExternalChannelId": "author_channel_id",
|
|
||||||
"authorNameTextColor": 2315255808,
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"liveChatItemContextMenuEndpoint": {
|
{
|
||||||
"params": "___params___"
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"purchaseAmountText": {
|
||||||
|
"simpleText": "¥846"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "dummy_message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"headerBackgroundColor": 4278239141,
|
||||||
|
"headerTextColor": 4278190080,
|
||||||
|
"bodyBackgroundColor": 4280150454,
|
||||||
|
"bodyTextColor": 4278190080,
|
||||||
|
"authorExternalChannelId": "author_channel_id",
|
||||||
|
"authorNameTextColor": 2315255808,
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timestampColor": 2147483648,
|
"liveChatItemContextMenuEndpoint": {
|
||||||
"contextMenuAccessibility": {
|
"params": "___params___"
|
||||||
"accessibilityData": {
|
}
|
||||||
"label": "コメントの操作"
|
},
|
||||||
}
|
"timestampColor": 2147483648,
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
|
},
|
||||||
"fullDurationSec": 120
|
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
|
||||||
}
|
"fullDurationSec": 120
|
||||||
},
|
}
|
||||||
"durationSec": "120"
|
},
|
||||||
}
|
"durationSec": "120"
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
174
tests/testdata/default/supersticker.json
vendored
174
tests/testdata/default/supersticker.json
vendored
@@ -1,99 +1,97 @@
|
|||||||
{
|
{
|
||||||
"response": {
|
"responseContext": {
|
||||||
"responseContext": {
|
"webResponseContextExtensionData": ""
|
||||||
"webResponseContextExtensionData": ""
|
},
|
||||||
},
|
"continuationContents": {
|
||||||
"continuationContents": {
|
"liveChatContinuation": {
|
||||||
"liveChatContinuation": {
|
"continuations": [
|
||||||
"continuations": [
|
{
|
||||||
{
|
"invalidationContinuationData": {
|
||||||
"invalidationContinuationData": {
|
"invalidationId": {
|
||||||
"invalidationId": {
|
"objectSource": 1000,
|
||||||
"objectSource": 1000,
|
"objectId": "___objectId___",
|
||||||
"objectId": "___objectId___",
|
"topic": "chat~00000000000~0000000",
|
||||||
"topic": "chat~00000000000~0000000",
|
"subscribeToGcmTopics": true,
|
||||||
"subscribeToGcmTopics": true,
|
"protoCreationTimestampMs": "1577804400000"
|
||||||
"protoCreationTimestampMs": "1577804400000"
|
},
|
||||||
},
|
"timeoutMs": 10000,
|
||||||
"timeoutMs": 10000,
|
"continuation": "___continuation___"
|
||||||
"continuation": "___continuation___"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"actions": [
|
],
|
||||||
{
|
"actions": [
|
||||||
"addChatItemAction": {
|
{
|
||||||
"item": {
|
"addChatItemAction": {
|
||||||
"liveChatPaidStickerRenderer": {
|
"item": {
|
||||||
"id": "dummy_id",
|
"liveChatPaidStickerRenderer": {
|
||||||
"contextMenuEndpoint": {
|
"id": "dummy_id",
|
||||||
"commandMetadata": {
|
"contextMenuEndpoint": {
|
||||||
"webCommandMetadata": {
|
"commandMetadata": {
|
||||||
"ignoreNavigation": true
|
"webCommandMetadata": {
|
||||||
}
|
"ignoreNavigation": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"liveChatItemContextMenuEndpoint": {
|
||||||
|
"params": "___params___"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestampUsec": 1570678496000000,
|
||||||
|
"authorPhoto": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 32,
|
||||||
|
"height": 32
|
||||||
},
|
},
|
||||||
"liveChatItemContextMenuEndpoint": {
|
{
|
||||||
"params": "___params___"
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
"contextMenuAccessibility": {
|
},
|
||||||
|
"authorName": {
|
||||||
|
"simpleText": "author_name"
|
||||||
|
},
|
||||||
|
"authorExternalChannelId": "author_channel_id",
|
||||||
|
"sticker": {
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "//lh3.googleusercontent.com/param_s=s72-rp",
|
||||||
|
"width": 72,
|
||||||
|
"height": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "//lh3.googleusercontent.com/param_s=s144-rp",
|
||||||
|
"width": 144,
|
||||||
|
"height": 144
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accessibility": {
|
||||||
"accessibilityData": {
|
"accessibilityData": {
|
||||||
"label": "コメントの操作"
|
"label": "___sticker_label___"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"timestampUsec": 1570678496000000,
|
},
|
||||||
"authorPhoto": {
|
"moneyChipBackgroundColor": 4278248959,
|
||||||
"thumbnails": [
|
"moneyChipTextColor": 4278190080,
|
||||||
{
|
"purchaseAmountText": {
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
"simpleText": "¥200"
|
||||||
"width": 32,
|
},
|
||||||
"height": 32
|
"stickerDisplayWidth": 72,
|
||||||
},
|
"stickerDisplayHeight": 72,
|
||||||
{
|
"backgroundColor": 4278237396,
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
"authorNameTextColor": 3003121664
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"authorName": {
|
|
||||||
"simpleText": "author_name"
|
|
||||||
},
|
|
||||||
"authorExternalChannelId": "author_channel_id",
|
|
||||||
"sticker": {
|
|
||||||
"thumbnails": [
|
|
||||||
{
|
|
||||||
"url": "//lh3.googleusercontent.com/param_s=s72-rp",
|
|
||||||
"width": 72,
|
|
||||||
"height": 72
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "//lh3.googleusercontent.com/param_s=s144-rp",
|
|
||||||
"width": 144,
|
|
||||||
"height": 144
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"accessibility": {
|
|
||||||
"accessibilityData": {
|
|
||||||
"label": "___sticker_label___"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"moneyChipBackgroundColor": 4278248959,
|
|
||||||
"moneyChipTextColor": 4278190080,
|
|
||||||
"purchaseAmountText": {
|
|
||||||
"simpleText": "¥200"
|
|
||||||
},
|
|
||||||
"stickerDisplayWidth": 72,
|
|
||||||
"stickerDisplayHeight": 72,
|
|
||||||
"backgroundColor": 4278237396,
|
|
||||||
"authorNameTextColor": 3003121664
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
136
tests/testdata/default/textmessage.json
vendored
136
tests/testdata/default/textmessage.json
vendored
@@ -1,79 +1,77 @@
|
|||||||
{
|
{
|
||||||
"response": {
|
"responseContext": {
|
||||||
"responseContext": {
|
"webResponseContextExtensionData": ""
|
||||||
"webResponseContextExtensionData": ""
|
},
|
||||||
},
|
"continuationContents": {
|
||||||
"continuationContents": {
|
"liveChatContinuation": {
|
||||||
"liveChatContinuation": {
|
"continuations": [
|
||||||
"continuations": [
|
{
|
||||||
{
|
"invalidationContinuationData": {
|
||||||
"invalidationContinuationData": {
|
"invalidationId": {
|
||||||
"invalidationId": {
|
"objectSource": 1000,
|
||||||
"objectSource": 1000,
|
"objectId": "___objectId___",
|
||||||
"objectId": "___objectId___",
|
"topic": "chat~00000000000~0000000",
|
||||||
"topic": "chat~00000000000~0000000",
|
"subscribeToGcmTopics": true,
|
||||||
"subscribeToGcmTopics": true,
|
"protoCreationTimestampMs": "1577804400000"
|
||||||
"protoCreationTimestampMs": "1577804400000"
|
},
|
||||||
},
|
"timeoutMs": 10000,
|
||||||
"timeoutMs": 10000,
|
"continuation": "___continuation___"
|
||||||
"continuation": "___continuation___"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"actions": [
|
],
|
||||||
{
|
"actions": [
|
||||||
"addChatItemAction": {
|
{
|
||||||
"item": {
|
"addChatItemAction": {
|
||||||
"liveChatTextMessageRenderer": {
|
"item": {
|
||||||
"message": {
|
"liveChatTextMessageRenderer": {
|
||||||
"runs": [
|
"message": {
|
||||||
{
|
"runs": [
|
||||||
"text": "dummy_message"
|
{
|
||||||
}
|
"text": "dummy_message"
|
||||||
]
|
}
|
||||||
},
|
]
|
||||||
"authorName": {
|
},
|
||||||
"simpleText": "author_name"
|
"authorName": {
|
||||||
},
|
"simpleText": "author_name"
|
||||||
"authorPhoto": {
|
},
|
||||||
"thumbnails": [
|
"authorPhoto": {
|
||||||
{
|
"thumbnails": [
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
{
|
||||||
"width": 32,
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
"height": 32
|
"width": 32,
|
||||||
},
|
"height": 32
|
||||||
{
|
|
||||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
|
||||||
"width": 64,
|
|
||||||
"height": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"contextMenuEndpoint": {
|
|
||||||
"commandMetadata": {
|
|
||||||
"webCommandMetadata": {
|
|
||||||
"ignoreNavigation": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"liveChatItemContextMenuEndpoint": {
|
{
|
||||||
"params": "___params___"
|
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||||
|
"width": 64,
|
||||||
|
"height": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contextMenuEndpoint": {
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"ignoreNavigation": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "dummy_id",
|
"liveChatItemContextMenuEndpoint": {
|
||||||
"timestampUsec": 1570678496000000,
|
"params": "___params___"
|
||||||
"authorExternalChannelId": "author_channel_id",
|
}
|
||||||
"contextMenuAccessibility": {
|
},
|
||||||
"accessibilityData": {
|
"id": "dummy_id",
|
||||||
"label": "コメントの操作"
|
"timestampUsec": 1570678496000000,
|
||||||
}
|
"authorExternalChannelId": "author_channel_id",
|
||||||
|
"contextMenuAccessibility": {
|
||||||
|
"accessibilityData": {
|
||||||
|
"label": "コメントの操作"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"clientId": "dummy_client_id"
|
},
|
||||||
}
|
"clientId": "dummy_client_id"
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12242
tests/testdata/extract_duplcheck/head/dp0-0.json
vendored
12242
tests/testdata/extract_duplcheck/head/dp0-0.json
vendored
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user