Compare commits
249 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc401ac80f | ||
|
|
0a9837adca | ||
|
|
f4bf30c0e9 | ||
|
|
acfba74821 | ||
|
|
f46845c777 | ||
|
|
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 | ||
|
|
4e829a25d4 | ||
|
|
15132a9bb8 | ||
|
|
64ace9dad6 | ||
|
|
9a2e96d3a0 | ||
|
|
a3695a59b8 | ||
|
|
bc8655ed62 | ||
|
|
3bdc465740 | ||
|
|
235d6b7212 | ||
|
|
9f0754da57 | ||
|
|
306b0a4564 | ||
|
|
1c49387f1a | ||
|
|
300d96e56c | ||
|
|
0e301f48a8 | ||
|
|
a790ab13a9 | ||
|
|
0456300d19 | ||
|
|
2ef1e7028f | ||
|
|
9413c4a186 | ||
|
|
8a8cef399f | ||
|
|
3bcad12cf6 | ||
|
|
4eb18279fe | ||
|
|
e9ed564e1b | ||
|
|
95f975c93d | ||
|
|
8012e1d191 | ||
|
|
404727c49c | ||
|
|
5f78a99507 | ||
|
|
497d84015e | ||
|
|
5d3c7b5abd | ||
|
|
f16ef60f11 | ||
|
|
5fa4d051ee |
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
|
||||
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"], version = "0.16.1"}
|
||||
|
||||
[dev-packages]
|
||||
pytest-mock = "*"
|
||||
pytest-httpx = "*"
|
||||
wheel = "*"
|
||||
twine = "*"
|
||||
424
Pipfile.lock
generated
Normal file
424
Pipfile.lock
generated
Normal file
@@ -0,0 +1,424 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "e1eb34f14c75998519a90838b283ccd23bd168afa8e4837f956c5c4df66376f9"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
||||
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
||||
],
|
||||
"version": "==2020.12.5"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||
],
|
||||
"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:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9",
|
||||
"sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"
|
||||
],
|
||||
"version": "==0.12.3"
|
||||
},
|
||||
"httpx": {
|
||||
"extras": [
|
||||
"http2"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537",
|
||||
"sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.16.1"
|
||||
},
|
||||
"hyperframe": {
|
||||
"hashes": [
|
||||
"sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40",
|
||||
"sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"
|
||||
],
|
||||
"version": "==5.2.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16",
|
||||
"sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"
|
||||
],
|
||||
"version": "==3.1"
|
||||
},
|
||||
"rfc3986": {
|
||||
"extras": [
|
||||
"idna2008"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
|
||||
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||
],
|
||||
"version": "==1.2.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||
],
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125",
|
||||
"sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
||||
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
||||
],
|
||||
"version": "==2020.12.5"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
|
||||
"sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d",
|
||||
"sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a",
|
||||
"sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec",
|
||||
"sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362",
|
||||
"sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668",
|
||||
"sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c",
|
||||
"sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b",
|
||||
"sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06",
|
||||
"sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698",
|
||||
"sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2",
|
||||
"sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c",
|
||||
"sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7",
|
||||
"sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009",
|
||||
"sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03",
|
||||
"sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b",
|
||||
"sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e",
|
||||
"sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909",
|
||||
"sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53",
|
||||
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
|
||||
"sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26",
|
||||
"sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b",
|
||||
"sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01",
|
||||
"sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb",
|
||||
"sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293",
|
||||
"sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd",
|
||||
"sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d",
|
||||
"sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3",
|
||||
"sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d",
|
||||
"sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e",
|
||||
"sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca",
|
||||
"sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d",
|
||||
"sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775",
|
||||
"sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375",
|
||||
"sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b",
|
||||
"sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b",
|
||||
"sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"
|
||||
],
|
||||
"version": "==1.14.4"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
],
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
||||
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
|
||||
],
|
||||
"version": "==0.4.4"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d",
|
||||
"sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7",
|
||||
"sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901",
|
||||
"sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c",
|
||||
"sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244",
|
||||
"sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6",
|
||||
"sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5",
|
||||
"sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e",
|
||||
"sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c",
|
||||
"sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0",
|
||||
"sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812",
|
||||
"sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a",
|
||||
"sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030",
|
||||
"sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"
|
||||
],
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
|
||||
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
|
||||
],
|
||||
"version": "==0.16"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9",
|
||||
"sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"
|
||||
],
|
||||
"version": "==0.12.3"
|
||||
},
|
||||
"httpx": {
|
||||
"extras": [
|
||||
"http2"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537",
|
||||
"sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.16.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16",
|
||||
"sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"
|
||||
],
|
||||
"version": "==3.1"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"jeepney": {
|
||||
"hashes": [
|
||||
"sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657",
|
||||
"sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae"
|
||||
],
|
||||
"markers": "sys_platform == 'linux'",
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"keyring": {
|
||||
"hashes": [
|
||||
"sha256:9acb3e1452edbb7544822b12fd25459078769e560fa51f418b6d00afaa6178df",
|
||||
"sha256:9f44660a5d4931bdc14c08a1d01ef30b18a7a8147380710d8c9f9531e1f6c3c0"
|
||||
],
|
||||
"version": "==22.0.1"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
],
|
||||
"version": "==20.9"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
"sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4",
|
||||
"sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75"
|
||||
],
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||
],
|
||||
"version": "==1.10.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||
],
|
||||
"version": "==2.20"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435",
|
||||
"sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"
|
||||
],
|
||||
"version": "==2.7.4"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9",
|
||||
"sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"
|
||||
],
|
||||
"version": "==6.2.2"
|
||||
},
|
||||
"pytest-httpx": {
|
||||
"hashes": [
|
||||
"sha256:0a7c56e559b23efbf857054cd74de60a7c540694a162423f89c70da6ad358d8e",
|
||||
"sha256:d32e8f6fb7e028f0313f5f5a2d463c8673eb43fd11a9bfe8527299717a7764c4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"pytest-mock": {
|
||||
"hashes": [
|
||||
"sha256:024e405ad382646318c4281948aadf6fe1135632bea9cc67366ea0c4098ef5f2",
|
||||
"sha256:a4d6d37329e4a893e77d9ffa89e838dd2b45d5dc099984cf03c703ac8411bb82"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d",
|
||||
"sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a"
|
||||
],
|
||||
"version": "==28.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"requests-toolbelt": {
|
||||
"hashes": [
|
||||
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
|
||||
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||
],
|
||||
"version": "==0.9.1"
|
||||
},
|
||||
"rfc3986": {
|
||||
"extras": [
|
||||
"idna2008"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
|
||||
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"secretstorage": {
|
||||
"hashes": [
|
||||
"sha256:30cfdef28829dad64d6ea1ed08f8eff6aa115a77068926bcc9f5225d5a3246aa",
|
||||
"sha256:5c36f6537a523ec5f969ef9fad61c98eb9e017bc601d811e53aa25bece64892f"
|
||||
],
|
||||
"markers": "sys_platform == 'linux'",
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||
],
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a",
|
||||
"sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65"
|
||||
],
|
||||
"version": "==4.56.0"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab",
|
||||
"sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80",
|
||||
"sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"
|
||||
],
|
||||
"version": "==1.26.3"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"wheel": {
|
||||
"hashes": [
|
||||
"sha256:906864fb722c0ab5f2f9c35b2c65e3af3c009402c108a709c0aca27bc2c9187b",
|
||||
"sha256:aaef9b8c36db72f8bf7f1e54f85f875c4d466819940863ca0b3f3f77f0a1646f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.36.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
176
README.md
176
README.md
@@ -2,12 +2,12 @@ pytchat
|
||||
=======
|
||||
|
||||
pytchat is a python library for fetching youtube live chat.
|
||||
|
||||
|
||||
<br><br><br>
|
||||
## Description
|
||||
pytchat is a python library for fetching youtube live chat
|
||||
without using youtube api, Selenium or BeautifulSoup.
|
||||
|
||||
pytchatは、YouTubeチャットを閲覧するためのpythonライブラリです。
|
||||
without using Selenium or BeautifulSoup.
|
||||
|
||||
Other features:
|
||||
+ 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.
|
||||
|
||||
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
|
||||
```python
|
||||
@@ -24,147 +24,44 @@ pip install pytchat
|
||||
```
|
||||
## Examples
|
||||
|
||||
### CLI
|
||||
|
||||
One-liner command.
|
||||
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
|
||||
### Fetch chat data (see [wiki](https://github.com/taizan-hokuto/pytchat/wiki/PytchatCore))
|
||||
```python
|
||||
from pytchat import LiveChat
|
||||
livechat = LiveChat(video_id = "Zvp1pJpie4I")
|
||||
# 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() )
|
||||
|
||||
import pytchat
|
||||
chat = pytchat.create(video_id="uIx8l2xlYVY")
|
||||
while chat.is_alive():
|
||||
try:
|
||||
data = chat.get()
|
||||
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()
|
||||
for c in chat.get().sync_items():
|
||||
print(f"{c.datetime} [{c.author.name}]- {c.message}")
|
||||
```
|
||||
### 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
|
||||
from pytchat import LiveChat
|
||||
import pytchat
|
||||
import time
|
||||
|
||||
def main():
|
||||
#seektime (seconds): start position of chat.
|
||||
chat = LiveChat("ojes5ULOqhc", seektime = 60*30)
|
||||
print('Replay from 30:00')
|
||||
try:
|
||||
while chat.is_alive():
|
||||
data = chat.get()
|
||||
for c in data.items:
|
||||
print(f"{c.elapsedTime} [{c.author.name}]-{c.message} {c.amountString}")
|
||||
data.tick()
|
||||
except KeyboardInterrupt:
|
||||
chat.terminate()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
chat = pytchat.create(video_id="uIx8l2xlYVY")
|
||||
while chat.is_alive():
|
||||
print(chat.get().json())
|
||||
time.sleep(5)
|
||||
'''
|
||||
# Each chat item can also be output in JSON format.
|
||||
for c in chat.get().items:
|
||||
print(c.json())
|
||||
'''
|
||||
```
|
||||
### 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()
|
||||
print("finished.")
|
||||
```
|
||||
### other
|
||||
+ 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
|
||||
Each item can be got with `items` function.
|
||||
Each item can be got with `sync_items()` function.
|
||||
<table>
|
||||
<tr>
|
||||
<th>name</th>
|
||||
@@ -291,16 +188,3 @@ Structure of author object.
|
||||
[](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.
|
||||
"""
|
||||
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
|
||||
__version__ = '0.1.4'
|
||||
__copyright__ = 'Copyright (C) 2019, 2020 taizan-hokuto'
|
||||
__version__ = '0.5.3'
|
||||
__license__ = 'MIT'
|
||||
__author__ = 'taizan-hokuto'
|
||||
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
||||
__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 (
|
||||
cli,
|
||||
config,
|
||||
LiveChat,
|
||||
LiveChatAsync,
|
||||
@@ -19,14 +33,12 @@ from .api import (
|
||||
CompatibleProcessor,
|
||||
DummyProcessor,
|
||||
DefaultProcessor,
|
||||
Extractor,
|
||||
HTMLArchiver,
|
||||
TSVArchiver,
|
||||
JsonfileArchiver,
|
||||
SimpleDisplayProcessor,
|
||||
SpeedCalculator,
|
||||
SuperchatCalculator,
|
||||
VideoInfo
|
||||
create
|
||||
)
|
||||
|
||||
# flake8: noqa
|
||||
@@ -1,5 +1,5 @@
|
||||
from . import cli
|
||||
from . import config
|
||||
from .core import create
|
||||
from .core_multithread.livechat import LiveChat
|
||||
from .core_async.livechat import LiveChatAsync
|
||||
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.speed.calculator import SpeedCalculator
|
||||
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
|
||||
@@ -1,74 +0,0 @@
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pytchat.util.extract_video_id import extract_video_id
|
||||
from .arguments import Arguments
|
||||
from .. exceptions import InvalidVideoIdException, NoContents, VideoInfoParseException
|
||||
from .. processors.html_archiver import HTMLArchiver
|
||||
from .. tool.extract.extractor import Extractor
|
||||
from .. tool.videoinfo import VideoInfo
|
||||
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')
|
||||
Arguments(parser.parse_args().__dict__)
|
||||
if Arguments().print_version:
|
||||
print(f'pytchat v{__version__} © 2019 taizan-hokuto')
|
||||
return
|
||||
|
||||
# Extractor
|
||||
if Arguments().video_ids:
|
||||
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()}")
|
||||
Extractor(video_id,
|
||||
processor=HTMLArchiver(
|
||||
Arguments().output + video_id + '.html'),
|
||||
callback=_disp_progress
|
||||
).extract()
|
||||
print("\nExtraction end.\n")
|
||||
except InvalidVideoIdException:
|
||||
print("Invalid Video ID or URL:", video_id)
|
||||
except (TypeError, NoContents) as e:
|
||||
print(e)
|
||||
except FileNotFoundError:
|
||||
print("The specified directory does not exist.:{}".format(Arguments().output))
|
||||
except VideoInfoParseException:
|
||||
print("Cannot parse video information.:{}".format(video_id))
|
||||
return
|
||||
parser.print_help()
|
||||
|
||||
|
||||
def _disp_progress(a, b):
|
||||
print('.', end="", flush=True)
|
||||
@@ -1,43 +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'
|
||||
|
||||
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] = []
|
||||
# Videos
|
||||
if arguments[Arguments.Name.VIDEO_IDS]:
|
||||
self.video_ids = [video_id
|
||||
for video_id in arguments[Arguments.Name.VIDEO_IDS].split(',')]
|
||||
|
||||
|
||||
|
||||
@@ -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,7 +1,12 @@
|
||||
import logging
|
||||
import logging # noqa
|
||||
from . import mylogger
|
||||
from base64 import a85decode as dc
|
||||
headers = {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 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)',
|
||||
}
|
||||
|
||||
_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):
|
||||
|
||||
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)
|
||||
216
pytchat/core/pytchat.py
Normal file
216
pytchat/core/pytchat.py
Normal file
@@ -0,0 +1,216 @@
|
||||
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
|
||||
|
||||
interruptable : bool
|
||||
Allows keyboard interrupts.
|
||||
Set this parameter to False if your own 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(),
|
||||
interruptable=True,
|
||||
force_replay=False,
|
||||
topchat_only=False,
|
||||
hold_exception=True,
|
||||
logger=config.logger(__name__),
|
||||
replay_continuation=None
|
||||
):
|
||||
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(httpx.Client(http2=True), 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:
|
||||
with httpx.Client(http2=True) as client:
|
||||
if self.continuation and self._is_alive:
|
||||
contents = self._get_contents(self.continuation, 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):
|
||||
with httpx.Client(http2=True) as client:
|
||||
try:
|
||||
response = client.post(self._fetch_url, json=param)
|
||||
livechat_json = json.loads(response.text)
|
||||
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):
|
||||
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):
|
||||
'''
|
||||
チャットデータを格納するバッファの役割を持つFIFOキュー
|
||||
Buffer for storing chat data.
|
||||
|
||||
Parameter
|
||||
---------
|
||||
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):
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
import aiohttp
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
import json
|
||||
import signal
|
||||
import time
|
||||
import traceback
|
||||
import urllib.parse
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from concurrent.futures import CancelledError
|
||||
from asyncio import Queue
|
||||
from concurrent.futures import CancelledError
|
||||
from .buffer import Buffer
|
||||
from ..parser.live import Parser
|
||||
from .. import config
|
||||
from .. import exceptions
|
||||
from .. import util
|
||||
from ..paramgen import liveparam, arcparam
|
||||
from ..processors.default.processor import DefaultProcessor
|
||||
from ..processors.combinator import Combinator
|
||||
from ..util.extract_video_id import extract_video_id
|
||||
|
||||
headers = config.headers
|
||||
MAX_RETRY = 10
|
||||
|
||||
|
||||
class LiveChatAsync:
|
||||
'''asyncio(aiohttp)を利用してYouTubeのライブ配信のチャットデータを取得する。
|
||||
'''LiveChatAsync object fetches chat data and stores them
|
||||
in a buffer with asyncio.
|
||||
|
||||
Parameter
|
||||
---------
|
||||
video_id : str
|
||||
動画ID
|
||||
|
||||
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
|
||||
チャットデータを加工するオブジェクト
|
||||
|
||||
buffer : Buffer(maxsize:20[default])
|
||||
チャットデータchat_componentを格納するバッファ。
|
||||
maxsize : 格納できるchat_componentの個数
|
||||
default値20個。1個で約5~10秒分。
|
||||
buffer : Buffer
|
||||
buffer of chat data fetched background.
|
||||
|
||||
interruptable : bool
|
||||
Ctrl+Cによる処理中断を行うかどうか。
|
||||
Allows keyboard interrupts.
|
||||
Set this parameter to False if your own threading program causes
|
||||
the problem.
|
||||
|
||||
callback : func
|
||||
_listen()関数から一定間隔で自動的に呼びだす関数。
|
||||
function called periodically from _listen().
|
||||
|
||||
done_callback : func
|
||||
listener終了時に呼び出すコールバック。
|
||||
function called when listener ends.
|
||||
|
||||
exception_handler : func
|
||||
例外を処理する関数
|
||||
|
||||
direct_mode : bool
|
||||
Trueの場合、bufferを使わずにcallbackを呼ぶ。
|
||||
Trueの場合、callbackの設定が必須
|
||||
(設定していない場合IllegalFunctionCall例外を発生させる)
|
||||
If True, invoke specified callback function without using buffer.
|
||||
callback is required. If not, IllegalFunctionCall will be raised.
|
||||
|
||||
force_replay : bool
|
||||
Trueの場合、ライブチャットが取得できる場合であっても
|
||||
強制的にアーカイブ済みチャットを取得する。
|
||||
force to fetch archived chat data, even if specified video is live.
|
||||
|
||||
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
|
||||
---------
|
||||
_is_alive : bool
|
||||
チャット取得を停止するためのフラグ
|
||||
Flag to stop getting chat.
|
||||
'''
|
||||
|
||||
_setup_finished = False
|
||||
@@ -86,8 +86,9 @@ class LiveChatAsync:
|
||||
force_replay=False,
|
||||
topchat_only=False,
|
||||
logger=config.logger(__name__),
|
||||
replay_continuation=None
|
||||
):
|
||||
self._video_id = extract_video_id(video_id)
|
||||
self._video_id = util.extract_video_id(video_id)
|
||||
self.seektime = seektime
|
||||
if isinstance(processor, tuple):
|
||||
self.processor = Combinator(processor)
|
||||
@@ -99,46 +100,48 @@ class LiveChatAsync:
|
||||
self._exception_handler = exception_handler
|
||||
self._direct_mode = direct_mode
|
||||
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._pauser = Queue()
|
||||
self._pauser.put_nowait(None)
|
||||
self._first_fetch = True
|
||||
self._fetch_url = "live_chat/get_live_chat?continuation="
|
||||
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.exception = None
|
||||
self.continuation = replay_continuation
|
||||
LiveChatAsync._logger = logger
|
||||
|
||||
if exception_handler:
|
||||
self._set_exception_handler(exception_handler)
|
||||
if interruptable:
|
||||
signal.signal(signal.SIGINT,
|
||||
(lambda a, b: asyncio.create_task(
|
||||
LiveChatAsync.shutdown(None, signal.SIGINT, b))))
|
||||
(lambda a, b: self._keyboard_interrupt()))
|
||||
self._setup()
|
||||
|
||||
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._callback is None:
|
||||
raise exceptions.IllegalFunctionCall(
|
||||
"When direct_mode=True, callback parameter is required.")
|
||||
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:
|
||||
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:
|
||||
pass
|
||||
else:
|
||||
# callbackを呼ぶループタスクの開始
|
||||
# Create a loop task to call callback if the `callback` param is specified.
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(self._callback_loop(self._callback))
|
||||
# _listenループタスクの開始
|
||||
# Start a loop task for _listen()
|
||||
loop = asyncio.get_event_loop()
|
||||
self.listen_task = loop.create_task(self._startlisten())
|
||||
# add_done_callbackの登録
|
||||
# Register add_done_callback
|
||||
if self._done_callback is None:
|
||||
self.listen_task.add_done_callback(self._finish)
|
||||
else:
|
||||
@@ -148,8 +151,13 @@ class LiveChatAsync:
|
||||
"""Fetch first continuation parameter,
|
||||
create and start _listen loop.
|
||||
"""
|
||||
initial_continuation = liveparam.getparam(self._video_id, 3)
|
||||
await self._listen(initial_continuation)
|
||||
if not self.continuation:
|
||||
self.continuation = liveparam.getparam(
|
||||
self._video_id,
|
||||
channel_id=util.get_channelid(httpx.Client(http2=True), self._video_id),
|
||||
past_sec=3)
|
||||
|
||||
await self._listen(self.continuation)
|
||||
|
||||
async def _listen(self, continuation):
|
||||
''' Fetch chat data and store them into buffer,
|
||||
@@ -161,13 +169,14 @@ class LiveChatAsync:
|
||||
parameter for next chat data
|
||||
'''
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with httpx.AsyncClient(http2=True) as client:
|
||||
while(continuation and self._is_alive):
|
||||
continuation = await self._check_pause(continuation)
|
||||
contents = await self._get_contents(
|
||||
continuation, session, headers)
|
||||
contents = await self._get_contents(continuation, client, headers)
|
||||
metadata, chatdata = self._parser.parse(contents)
|
||||
|
||||
continuation = metadata.get('continuation')
|
||||
if continuation:
|
||||
self.continuation = continuation
|
||||
timeout = metadata['timeoutMs'] / 1000
|
||||
chat_component = {
|
||||
"video_id": self._video_id,
|
||||
@@ -186,16 +195,15 @@ class LiveChatAsync:
|
||||
await self._buffer.put(chat_component)
|
||||
diff_time = timeout - (time.time() - time_mark)
|
||||
await asyncio.sleep(diff_time)
|
||||
continuation = metadata.get('continuation')
|
||||
self._last_offset_ms = metadata.get('last_offset_ms', 0)
|
||||
except exceptions.ChatParseException as e:
|
||||
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
||||
raise
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
self._logger.error(f"{traceback.format_exc(limit = -1)}")
|
||||
except Exception:
|
||||
self._logger.error(f"{traceback.format_exc(limit=-1)}")
|
||||
raise
|
||||
|
||||
self._logger.debug(f"[{self._video_id}]finished fetching chat.")
|
||||
raise exceptions.ChatDataFinished
|
||||
self._logger.debug(f"[{self._video_id}] finished fetching chat.")
|
||||
|
||||
async def _check_pause(self, continuation):
|
||||
if self._pauser.empty():
|
||||
@@ -206,11 +214,14 @@ class LiveChatAsync:
|
||||
'''
|
||||
self._pauser.put_nowait(None)
|
||||
if not self._is_replay:
|
||||
continuation = liveparam.getparam(
|
||||
self._video_id, 3, self._topchat_only)
|
||||
async with httpx.AsyncClient(http2=True) as client:
|
||||
continuation = await liveparam.getparam(self._video_id,
|
||||
channel_id=util.get_channelid_async(client, self.video_id),
|
||||
past_sec=3)
|
||||
|
||||
return continuation
|
||||
|
||||
async def _get_contents(self, continuation, session, headers):
|
||||
async 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.
|
||||
@@ -219,58 +230,61 @@ class LiveChatAsync:
|
||||
-------
|
||||
'continuationContents' which includes metadata & chatdata.
|
||||
'''
|
||||
livechat_json = await self._get_livechat_json(continuation, session, headers)
|
||||
contents = self._parser.get_contents(livechat_json)
|
||||
livechat_json = await 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 = "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(
|
||||
self._video_id, self.seektime, self._topchat_only)
|
||||
self._video_id, self.seektime, self._topchat_only, channelid)
|
||||
livechat_json = (await self._get_livechat_json(
|
||||
continuation, session, headers))
|
||||
continuation, client, replay=True, offset_ms=self.seektime * 1000))
|
||||
reload_continuation = self._parser.reload_continuation(
|
||||
self._parser.get_contents(livechat_json))
|
||||
self._parser.get_contents(livechat_json)[0])
|
||||
if reload_continuation:
|
||||
livechat_json = (await self._get_livechat_json(
|
||||
reload_continuation, session, headers))
|
||||
contents = self._parser.get_contents(livechat_json)
|
||||
reload_continuation, client, headers))
|
||||
contents, _ = self._parser.get_contents(livechat_json)
|
||||
self._is_replay = True
|
||||
self._first_fetch = False
|
||||
return contents
|
||||
|
||||
async def _get_livechat_json(self, continuation, session, headers):
|
||||
async def _get_livechat_json(self, continuation, client, replay: bool, offset_ms: int = 0):
|
||||
'''
|
||||
Get json which includes chat data.
|
||||
'''
|
||||
continuation = urllib.parse.quote(continuation)
|
||||
livechat_json = None
|
||||
status_code = 0
|
||||
url = f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1"
|
||||
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):
|
||||
async with session.get(url, headers=headers) as resp:
|
||||
try:
|
||||
text = await resp.text()
|
||||
livechat_json = json.loads(text)
|
||||
break
|
||||
except (ClientConnectorError, json.JSONDecodeError):
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
try:
|
||||
resp = await client.post(self._fetch_url, json=param)
|
||||
livechat_json = resp.json()
|
||||
break
|
||||
except (json.JSONDecodeError, httpx.HTTPError):
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
else:
|
||||
self._logger.error(f"[{self._video_id}]"
|
||||
f"Exceeded retry count. status_code={status_code}")
|
||||
return None
|
||||
f"Exceeded retry count.")
|
||||
raise exceptions.RetryExceedMaxCount()
|
||||
return livechat_json
|
||||
|
||||
async def _callback_loop(self, callback):
|
||||
""" コンストラクタでcallbackを指定している場合、バックグラウンドで
|
||||
callbackに指定された関数に一定間隔でチャットデータを投げる。
|
||||
""" If a callback is specified in the constructor,
|
||||
it throws chat data at regular intervals to the
|
||||
function specified in the callback in the backgroun
|
||||
|
||||
Parameter
|
||||
---------
|
||||
callback : func
|
||||
加工済みのチャットデータを渡す先の関数。
|
||||
function to which the processed chat data is passed.
|
||||
"""
|
||||
while self.is_alive():
|
||||
items = await self._buffer.get()
|
||||
@@ -281,11 +295,13 @@ class LiveChatAsync:
|
||||
await self._callback(processed_chat)
|
||||
|
||||
async def get(self):
|
||||
""" bufferからデータを取り出し、processorに投げ、
|
||||
加工済みのチャットデータを返す。
|
||||
"""
|
||||
Retrieves data from the buffer,
|
||||
throws it to the processor,
|
||||
and returns the processed chat data.
|
||||
|
||||
Returns
|
||||
: Processorによって加工されたチャットデータ
|
||||
: Chat data processed by the Processor
|
||||
"""
|
||||
if self._callback is None:
|
||||
if self.is_alive():
|
||||
@@ -294,7 +310,7 @@ class LiveChatAsync:
|
||||
else:
|
||||
return []
|
||||
raise exceptions.IllegalFunctionCall(
|
||||
"既にcallbackを登録済みのため、get()は実行できません。")
|
||||
"Callback parameter is already set, so get() cannot be performed.")
|
||||
|
||||
def is_replay(self):
|
||||
return self._is_replay
|
||||
@@ -315,11 +331,11 @@ class LiveChatAsync:
|
||||
return self._is_alive
|
||||
|
||||
def _finish(self, sender):
|
||||
'''Listener終了時のコールバック'''
|
||||
'''Called when the _listen() task finished.'''
|
||||
try:
|
||||
self._task_finished()
|
||||
except CancelledError:
|
||||
self._logger.debug(f'[{self._video_id}]cancelled:{sender}')
|
||||
self._logger.debug(f'[{self._video_id}] cancelled:{sender}')
|
||||
|
||||
def terminate(self):
|
||||
if self._pauser.empty():
|
||||
@@ -327,11 +343,12 @@ class LiveChatAsync:
|
||||
self._is_alive = False
|
||||
self._buffer.put_nowait({})
|
||||
self.processor.finalize()
|
||||
|
||||
|
||||
def _keyboard_interrupt(self):
|
||||
self.exception = exceptions.ChatDataFinished()
|
||||
self.terminate()
|
||||
|
||||
def _task_finished(self):
|
||||
'''
|
||||
Listenerを終了する。
|
||||
'''
|
||||
if self.is_alive():
|
||||
self.terminate()
|
||||
try:
|
||||
@@ -340,7 +357,7 @@ class LiveChatAsync:
|
||||
self.exception = e
|
||||
if not isinstance(e, exceptions.ChatParseException):
|
||||
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):
|
||||
if self.exception is not None:
|
||||
@@ -350,15 +367,3 @@ class LiveChatAsync:
|
||||
def _set_exception_handler(cls, handler):
|
||||
loop = asyncio.get_event_loop()
|
||||
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):
|
||||
'''
|
||||
チャットデータを格納するバッファの役割を持つFIFOキュー
|
||||
Buffer for storing chat data.
|
||||
|
||||
Parameter
|
||||
---------
|
||||
max_size : int
|
||||
格納するチャットブロックの最大個数。0の場合は無限。
|
||||
最大値を超える場合は古いチャットブロックから破棄される。
|
||||
maxsize : int
|
||||
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):
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import requests
|
||||
import httpx
|
||||
import json
|
||||
import signal
|
||||
import time
|
||||
import traceback
|
||||
import urllib.parse
|
||||
from concurrent.futures import CancelledError, ThreadPoolExecutor
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
@@ -11,64 +10,67 @@ from .buffer import Buffer
|
||||
from ..parser.live import Parser
|
||||
from .. import config
|
||||
from .. import exceptions
|
||||
from .. import util
|
||||
from ..paramgen import liveparam, arcparam
|
||||
from ..processors.default.processor import DefaultProcessor
|
||||
from ..processors.combinator import Combinator
|
||||
from ..util.extract_video_id import extract_video_id
|
||||
|
||||
headers = config.headers
|
||||
MAX_RETRY = 10
|
||||
|
||||
|
||||
class LiveChat:
|
||||
''' スレッドプールを利用してYouTubeのライブ配信のチャットデータを取得する
|
||||
'''
|
||||
LiveChat object fetches chat data and stores them
|
||||
in a buffer with ThreadpoolExecutor.
|
||||
|
||||
Parameter
|
||||
---------
|
||||
video_id : str
|
||||
動画ID
|
||||
|
||||
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
|
||||
チャットデータを加工するオブジェクト
|
||||
|
||||
buffer : Buffer(maxsize:20[default])
|
||||
チャットデータchat_componentを格納するバッファ。
|
||||
maxsize : 格納できるchat_componentの個数
|
||||
default値20個。1個で約5~10秒分。
|
||||
buffer : Buffer
|
||||
buffer of chat data fetched background.
|
||||
|
||||
interruptable : bool
|
||||
Ctrl+Cによる処理中断を行うかどうか。
|
||||
Allows keyboard interrupts.
|
||||
Set this parameter to False if your own threading program causes
|
||||
the problem.
|
||||
|
||||
callback : func
|
||||
_listen()関数から一定間隔で自動的に呼びだす関数。
|
||||
function called periodically from _listen().
|
||||
|
||||
done_callback : func
|
||||
listener終了時に呼び出すコールバック。
|
||||
function called when listener ends.
|
||||
|
||||
direct_mode : bool
|
||||
Trueの場合、bufferを使わずにcallbackを呼ぶ。
|
||||
Trueの場合、callbackの設定が必須
|
||||
(設定していない場合IllegalFunctionCall例外を発生させる)
|
||||
If True, invoke specified callback function without using buffer.
|
||||
callback is required. If not, IllegalFunctionCall will be raised.
|
||||
|
||||
force_replay : bool
|
||||
Trueの場合、ライブチャットが取得できる場合であっても
|
||||
強制的にアーカイブ済みチャットを取得する。
|
||||
force to fetch archived chat data, even if specified video is live.
|
||||
|
||||
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
|
||||
---------
|
||||
_executor : ThreadPoolExecutor
|
||||
チャットデータ取得ループ(_listen)用のスレッド
|
||||
This is used for _listen() loop.
|
||||
|
||||
_is_alive : bool
|
||||
チャット取得を停止するためのフラグ
|
||||
Flag to stop getting chat.
|
||||
'''
|
||||
|
||||
_setup_finished = False
|
||||
@@ -83,9 +85,10 @@ class LiveChat:
|
||||
direct_mode=False,
|
||||
force_replay=False,
|
||||
topchat_only=False,
|
||||
logger=config.logger(__name__)
|
||||
logger=config.logger(__name__),
|
||||
replay_continuation=None
|
||||
):
|
||||
self._video_id = extract_video_id(video_id)
|
||||
self._video_id = util.extract_video_id(video_id)
|
||||
self.seektime = seektime
|
||||
if isinstance(processor, tuple):
|
||||
self.processor = Combinator(processor)
|
||||
@@ -97,39 +100,43 @@ class LiveChat:
|
||||
self._executor = ThreadPoolExecutor(max_workers=2)
|
||||
self._direct_mode = direct_mode
|
||||
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._pauser = Queue()
|
||||
self._pauser.put_nowait(None)
|
||||
self._first_fetch = True
|
||||
self._fetch_url = "live_chat/get_live_chat?continuation="
|
||||
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._event = Event()
|
||||
self._dat = ''
|
||||
self._last_offset_ms = 0
|
||||
self._logger = logger
|
||||
self._event = Event()
|
||||
self.continuation = replay_continuation
|
||||
|
||||
self.exception = None
|
||||
if interruptable:
|
||||
signal.signal(signal.SIGINT, lambda a, b: self.terminate())
|
||||
self._setup()
|
||||
|
||||
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._callback is None:
|
||||
raise exceptions.IllegalFunctionCall(
|
||||
"When direct_mode=True, callback parameter is required.")
|
||||
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:
|
||||
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:
|
||||
pass
|
||||
else:
|
||||
# callbackを呼ぶループタスクの開始
|
||||
# Start a loop task calling callback function.
|
||||
self._executor.submit(self._callback_loop, self._callback)
|
||||
# _listenループタスクの開始
|
||||
# Start a loop task for _listen()
|
||||
self.listen_task = self._executor.submit(self._startlisten)
|
||||
# add_done_callbackの登録
|
||||
# Register add_done_callback
|
||||
if self._done_callback is None:
|
||||
self.listen_task.add_done_callback(self._finish)
|
||||
else:
|
||||
@@ -140,8 +147,12 @@ class LiveChat:
|
||||
"""Fetch first continuation parameter,
|
||||
create and start _listen loop.
|
||||
"""
|
||||
initial_continuation = liveparam.getparam(self._video_id, 3)
|
||||
self._listen(initial_continuation)
|
||||
if not self.continuation:
|
||||
self.continuation = liveparam.getparam(
|
||||
self._video_id,
|
||||
channel_id=util.get_channelid(httpx.Client(http2=True), self._video_id),
|
||||
past_sec=3)
|
||||
self._listen(self.continuation)
|
||||
|
||||
def _listen(self, continuation):
|
||||
''' Fetch chat data and store them into buffer,
|
||||
@@ -153,11 +164,14 @@ class LiveChat:
|
||||
parameter for next chat data
|
||||
'''
|
||||
try:
|
||||
with requests.Session() as session:
|
||||
with httpx.Client(http2=True) as client:
|
||||
while(continuation and self._is_alive):
|
||||
continuation = self._check_pause(continuation)
|
||||
contents = self._get_contents(continuation, session, headers)
|
||||
contents = self._get_contents(continuation, client, headers)
|
||||
metadata, chatdata = self._parser.parse(contents)
|
||||
continuation = metadata.get('continuation')
|
||||
if continuation:
|
||||
self.continuation = continuation
|
||||
timeout = metadata['timeoutMs'] / 1000
|
||||
chat_component = {
|
||||
"video_id": self._video_id,
|
||||
@@ -176,16 +190,15 @@ class LiveChat:
|
||||
self._buffer.put(chat_component)
|
||||
diff_time = timeout - (time.time() - time_mark)
|
||||
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:
|
||||
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
||||
raise
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
except Exception:
|
||||
self._logger.error(f"{traceback.format_exc(limit=-1)}")
|
||||
raise
|
||||
|
||||
self._logger.debug(f"[{self._video_id}]finished fetching chat.")
|
||||
raise exceptions.ChatDataFinished
|
||||
self._logger.debug(f"[{self._video_id}] finished fetching chat.")
|
||||
|
||||
def _check_pause(self, continuation):
|
||||
if self._pauser.empty():
|
||||
@@ -196,10 +209,13 @@ class LiveChat:
|
||||
'''
|
||||
self._pauser.put_nowait(None)
|
||||
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
|
||||
|
||||
def _get_contents(self, continuation, session, headers):
|
||||
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.
|
||||
@@ -208,59 +224,60 @@ class LiveChat:
|
||||
-------
|
||||
'continuationContents' which includes metadata & chat data.
|
||||
'''
|
||||
livechat_json = (
|
||||
self._get_livechat_json(continuation, session, headers)
|
||||
)
|
||||
contents = self._parser.get_contents(livechat_json)
|
||||
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 = "live_chat_replay/get_live_chat_replay?continuation="
|
||||
self._fetch_url = config._smr
|
||||
continuation = arcparam.getparam(
|
||||
self._video_id, self.seektime, self._topchat_only)
|
||||
livechat_json = (self._get_livechat_json(continuation, session, headers))
|
||||
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))
|
||||
self._parser.get_contents(livechat_json)[0])
|
||||
if reload_continuation:
|
||||
livechat_json = (self._get_livechat_json(
|
||||
reload_continuation, session, headers))
|
||||
contents = self._parser.get_contents(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, session, headers):
|
||||
def _get_livechat_json(self, continuation, client, replay: bool, offset_ms: int = 0):
|
||||
'''
|
||||
Get json which includes chat data.
|
||||
'''
|
||||
continuation = urllib.parse.quote(continuation)
|
||||
livechat_json = None
|
||||
status_code = 0
|
||||
url = f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1"
|
||||
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):
|
||||
with session.get(url, headers=headers) as resp:
|
||||
try:
|
||||
text = resp.text
|
||||
livechat_json = json.loads(text)
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
time.sleep(1)
|
||||
continue
|
||||
try:
|
||||
response = client.post(self._fetch_url, json=param)
|
||||
livechat_json = response.json()
|
||||
break
|
||||
except (json.JSONDecodeError, httpx.HTTPError):
|
||||
time.sleep(2)
|
||||
continue
|
||||
else:
|
||||
self._logger.error(f"[{self._video_id}]"
|
||||
f"Exceeded retry count. status_code={status_code}")
|
||||
f"Exceeded retry count.")
|
||||
raise exceptions.RetryExceedMaxCount()
|
||||
return livechat_json
|
||||
|
||||
def _callback_loop(self, callback):
|
||||
""" コンストラクタでcallbackを指定している場合、バックグラウンドで
|
||||
callbackに指定された関数に一定間隔でチャットデータを投げる。
|
||||
""" If a callback is specified in the constructor,
|
||||
it throws chat data at regular intervals to the
|
||||
function specified in the callback in the backgroun
|
||||
|
||||
Parameter
|
||||
---------
|
||||
callback : func
|
||||
加工済みのチャットデータを渡す先の関数。
|
||||
function to which the processed chat data is passed.
|
||||
"""
|
||||
while self.is_alive():
|
||||
items = self._buffer.get()
|
||||
@@ -271,11 +288,13 @@ class LiveChat:
|
||||
self._callback(processed_chat)
|
||||
|
||||
def get(self):
|
||||
""" bufferからデータを取り出し、processorに投げ、
|
||||
加工済みのチャットデータを返す。
|
||||
"""
|
||||
Retrieves data from the buffer,
|
||||
throws it to the processor,
|
||||
and returns the processed chat data.
|
||||
|
||||
Returns
|
||||
: Processorによって加工されたチャットデータ
|
||||
: Chat data processed by the Processor
|
||||
"""
|
||||
if self._callback is None:
|
||||
if self.is_alive():
|
||||
@@ -284,7 +303,7 @@ class LiveChat:
|
||||
else:
|
||||
return []
|
||||
raise exceptions.IllegalFunctionCall(
|
||||
"既にcallbackを登録済みのため、get()は実行できません。")
|
||||
"Callback parameter is already set, so get() cannot be performed.")
|
||||
|
||||
def is_replay(self):
|
||||
return self._is_replay
|
||||
@@ -305,11 +324,11 @@ class LiveChat:
|
||||
return self._is_alive
|
||||
|
||||
def _finish(self, sender):
|
||||
'''Listener終了時のコールバック'''
|
||||
'''Called when the _listen() task finished.'''
|
||||
try:
|
||||
self._task_finished()
|
||||
except CancelledError:
|
||||
self._logger.debug(f'[{self._video_id}]cancelled:{sender}')
|
||||
self._logger.debug(f'[{self._video_id}] cancelled:{sender}')
|
||||
|
||||
def terminate(self):
|
||||
if self._pauser.empty():
|
||||
@@ -320,9 +339,6 @@ class LiveChat:
|
||||
self.processor.finalize()
|
||||
|
||||
def _task_finished(self):
|
||||
'''
|
||||
Listenerを終了する。
|
||||
'''
|
||||
if self.is_alive():
|
||||
self.terminate()
|
||||
try:
|
||||
@@ -331,7 +347,7 @@ class LiveChat:
|
||||
self.exception = e
|
||||
if not isinstance(e, exceptions.ChatParseException):
|
||||
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):
|
||||
if self.exception is not None:
|
||||
|
||||
@@ -38,7 +38,9 @@ class InvalidVideoIdException(Exception):
|
||||
'''
|
||||
Thrown when the video_id is not exist (VideoInfo).
|
||||
'''
|
||||
pass
|
||||
def __init__(self, doc):
|
||||
self.msg = "InvalidVideoIdException"
|
||||
self.doc = doc
|
||||
|
||||
|
||||
class UnknownConnectionError(Exception):
|
||||
@@ -47,7 +49,7 @@ class UnknownConnectionError(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
|
||||
|
||||
@@ -64,7 +66,16 @@ class FailedExtractContinuation(ChatDataFinished):
|
||||
pass
|
||||
|
||||
|
||||
class VideoInfoParseException(Exception):
|
||||
class VideoInfoParseError(Exception):
|
||||
'''
|
||||
thrown when failed to parse video info
|
||||
Base exception when parsing video info.
|
||||
'''
|
||||
|
||||
|
||||
class PatternUnmatchError(VideoInfoParseError):
|
||||
'''
|
||||
Thrown when failed to parse video info with unmatched pattern.
|
||||
'''
|
||||
def __init__(self, doc=''):
|
||||
self.msg = "PatternUnmatchError"
|
||||
self.doc = doc
|
||||
|
||||
@@ -1,55 +1,37 @@
|
||||
from .pb.header_pb2 import Header
|
||||
from .pb.replay_pb2 import Continuation
|
||||
from . import enc
|
||||
from base64 import urlsafe_b64encode as b64enc
|
||||
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:
|
||||
header = Header()
|
||||
header.info.video.id = video_id
|
||||
header.terminator = 1
|
||||
return base64.urlsafe_b64encode(header.SerializeToString()).decode()
|
||||
def _header(video_id, channel_id) -> str:
|
||||
S1_3 = enc.rs(1, video_id)
|
||||
S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id)
|
||||
S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5)
|
||||
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:
|
||||
chattype = 1
|
||||
timestamp = 0
|
||||
if topchat_only:
|
||||
chattype = 4
|
||||
|
||||
fetch_before_start = 3
|
||||
def _build(video_id, seektime, topchat_only, channel_id) -> str:
|
||||
chattype = 4 if topchat_only else 1
|
||||
if seektime < 0:
|
||||
fetch_before_start = 4
|
||||
elif seektime == 0:
|
||||
timestamp = 1
|
||||
else:
|
||||
timestamp = int(seektime * 1000000)
|
||||
continuation = Continuation()
|
||||
entity = continuation.entity
|
||||
entity.header = _gen_vid(video_id)
|
||||
entity.timestamp = timestamp
|
||||
entity.s6 = 0
|
||||
entity.s7 = 0
|
||||
entity.s8 = 0
|
||||
entity.s9 = fetch_before_start
|
||||
entity.s10 = ''
|
||||
entity.s12 = chattype
|
||||
entity.chattype.value = chattype
|
||||
entity.s15 = 0
|
||||
return quote(
|
||||
base64.urlsafe_b64encode(continuation.SerializeToString()).decode())
|
||||
seektime = 0
|
||||
timestamp = int(seektime * 1000000)
|
||||
header = enc.rs(3, _header(video_id, channel_id))
|
||||
timestamp = enc.nm(5, timestamp)
|
||||
s6 = enc.nm(6, 0)
|
||||
s7 = enc.nm(7, 0)
|
||||
s8 = enc.nm(8, 0)
|
||||
s9 = enc.nm(9, 4)
|
||||
s10 = enc.rs(10, enc.nm(4, 0))
|
||||
chattype = enc.rs(14, enc.nm(1, 4))
|
||||
s15 = enc.nm(15, 0)
|
||||
entity = b''.join((header, timestamp, s6, s7, s8, s9, s10, chattype, s15))
|
||||
continuation = enc.rs(156074452, entity)
|
||||
return quote(b64enc(continuation).decode())
|
||||
|
||||
|
||||
def getparam(video_id, seektime=-1, topchat_only=False) -> str:
|
||||
def getparam(video_id, seektime=0, topchat_only=False, channel_id='') -> str:
|
||||
'''
|
||||
Parameter
|
||||
---------
|
||||
@@ -59,4 +41,4 @@ def getparam(video_id, seektime=-1, topchat_only=False) -> str:
|
||||
topchat_only : bool
|
||||
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 time
|
||||
|
||||
'''
|
||||
Generate continuation parameter of youtube live chat.
|
||||
|
||||
Author: taizan-hokuto
|
||||
|
||||
ver 0.0.1 2019.10.05 : Initial release.
|
||||
ver 0.0.2 2020.05.30 : Use Protocol Buffers.
|
||||
'''
|
||||
from . import enc
|
||||
from base64 import urlsafe_b64encode as b64enc
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def _gen_vid(video_id) -> str:
|
||||
"""generate video_id parameter.
|
||||
Parameter
|
||||
---------
|
||||
video_id : str
|
||||
|
||||
Return
|
||||
---------
|
||||
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 _header(video_id, channel_id) -> str:
|
||||
S1_3 = enc.rs(1, video_id)
|
||||
S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id)
|
||||
S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5)
|
||||
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, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
|
||||
chattype = 1
|
||||
if topchat_only:
|
||||
chattype = 4
|
||||
continuation = Continuation()
|
||||
entity = continuation.entity
|
||||
def _build(video_id, channel_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
|
||||
chattype = 4 if topchat_only else 1
|
||||
|
||||
entity.header = _gen_vid(video_id)
|
||||
entity.timestamp1 = ts1
|
||||
entity.s6 = 0
|
||||
entity.s7 = 0
|
||||
entity.s8 = 1
|
||||
entity.body.b1 = 0
|
||||
entity.body.b2 = 0
|
||||
entity.body.b3 = 0
|
||||
entity.body.b4 = 0
|
||||
entity.body.b7 = ''
|
||||
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
|
||||
b1 = enc.nm(1, 0)
|
||||
b2 = enc.nm(2, 0)
|
||||
b3 = enc.nm(3, 0)
|
||||
b4 = enc.nm(4, 0)
|
||||
b7 = enc.rs(7, '')
|
||||
b8 = enc.nm(8, 0)
|
||||
b9 = enc.rs(9, '')
|
||||
timestamp2 = enc.nm(10, ts2)
|
||||
b11 = enc.nm(11, 3)
|
||||
b15 = enc.nm(15, 0)
|
||||
|
||||
return quote(
|
||||
base64.urlsafe_b64encode(continuation.SerializeToString()).decode()
|
||||
)
|
||||
header = enc.rs(3, _header(video_id, channel_id))
|
||||
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):
|
||||
@@ -76,7 +58,7 @@ def _times(past_sec):
|
||||
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
|
||||
---------
|
||||
@@ -85,4 +67,4 @@ def getparam(video_id, past_sec=0, topchat_only=False) -> str:
|
||||
topchat_only : bool
|
||||
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:
|
||||
'''
|
||||
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.exception_holder = exception_holder
|
||||
|
||||
def get_contents(self, jsn):
|
||||
if jsn is None:
|
||||
raise exceptions.IllegalFunctionCall('Called with none JSON object.')
|
||||
if jsn['response']['responseContext'].get('errors'):
|
||||
self.raise_exception(exceptions.IllegalFunctionCall('Called with none JSON object.'))
|
||||
if jsn.get("responseContext", {}).get("errors"):
|
||||
raise exceptions.ResponseContextError(
|
||||
'The video_id would be wrong, or video is deleted or private.')
|
||||
contents = jsn['response'].get('continuationContents')
|
||||
return contents
|
||||
contents = jsn.get('continuationContents')
|
||||
visitor_data = jsn.get("responseContext", {}).get("visitorData")
|
||||
return contents, visitor_data
|
||||
|
||||
def parse(self, contents):
|
||||
"""
|
||||
@@ -42,11 +54,11 @@ class Parser:
|
||||
|
||||
if contents is None:
|
||||
'''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]
|
||||
if cont is None:
|
||||
raise exceptions.NoContinuation('No Continuation')
|
||||
self.raise_exception(exceptions.NoContinuation('No Continuation'))
|
||||
metadata = (cont.get('invalidationContinuationData')
|
||||
or cont.get('timedContinuationData')
|
||||
or cont.get('reloadContinuationData')
|
||||
@@ -54,13 +66,13 @@ class Parser:
|
||||
)
|
||||
if metadata is None:
|
||||
if cont.get("playerSeekContinuationData"):
|
||||
raise exceptions.ChatDataFinished('Finished chat data')
|
||||
self.raise_exception(exceptions.ChatDataFinished('Finished chat data'))
|
||||
unknown = list(cont.keys())[0]
|
||||
if unknown:
|
||||
raise exceptions.ReceivedUnknownContinuation(
|
||||
f"Received unknown continuation type:{unknown}")
|
||||
self.raise_exception(exceptions.ReceivedUnknownContinuation(
|
||||
f"Received unknown continuation type:{unknown}"))
|
||||
else:
|
||||
raise exceptions.FailedExtractContinuation('Cannot extract continuation data')
|
||||
self.raise_exception(exceptions.FailedExtractContinuation('Cannot extract continuation data'))
|
||||
return self._create_data(metadata, contents)
|
||||
|
||||
def reload_continuation(self, contents):
|
||||
@@ -72,8 +84,9 @@ class Parser:
|
||||
"""
|
||||
if contents is None:
|
||||
'''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]
|
||||
|
||||
if cont.get("liveChatReplayContinuationData"):
|
||||
# chat data exist.
|
||||
return None
|
||||
@@ -81,25 +94,29 @@ class Parser:
|
||||
init_cont = cont.get("playerSeekContinuationData")
|
||||
if init_cont:
|
||||
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):
|
||||
actions = contents['liveChatContinuation'].get('actions')
|
||||
if self.is_replay:
|
||||
interval = self._get_interval(actions)
|
||||
metadata.setdefault("timeoutMs", interval)
|
||||
last_offset_ms = self._get_lastoffset(actions)
|
||||
metadata.setdefault("timeoutMs", 5000)
|
||||
metadata.setdefault("last_offset_ms", last_offset_ms)
|
||||
"""Archived chat has different structures than live chat,
|
||||
so make it the same format."""
|
||||
chatdata = [action["replayChatItemAction"]["actions"][0]
|
||||
for action in actions]
|
||||
else:
|
||||
metadata.setdefault('timeoutMs', 10000)
|
||||
metadata.setdefault('timeoutMs', 5000)
|
||||
chatdata = actions
|
||||
return metadata, chatdata
|
||||
|
||||
def _get_interval(self, actions: list):
|
||||
if actions is None:
|
||||
return 0
|
||||
start = int(actions[0]["replayChatItemAction"]["videoOffsetTimeMsec"])
|
||||
last = int(actions[-1]["replayChatItemAction"]["videoOffsetTimeMsec"])
|
||||
return (last - start)
|
||||
def _get_lastoffset(self, actions: list):
|
||||
if actions:
|
||||
return int(actions[-1]["replayChatItemAction"]["videoOffsetTimeMsec"])
|
||||
return 0
|
||||
|
||||
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)
|
||||
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
|
||||
import pytz
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
TZ_UTC = timezone(timedelta(0), 'UTC')
|
||||
|
||||
|
||||
class BaseRenderer:
|
||||
@@ -62,13 +63,13 @@ class BaseRenderer:
|
||||
if badges:
|
||||
for badge in badges:
|
||||
author_type = badge["liveChatAuthorBadgeRenderer"]["accessibility"]["accessibilityData"]["label"]
|
||||
if author_type == '確認済み':
|
||||
if author_type == 'VERIFIED' or author_type == '確認済み':
|
||||
isVerified = True
|
||||
if author_type == '所有者':
|
||||
if author_type == 'OWNER' or author_type == '所有者':
|
||||
isChatOwner = True
|
||||
if 'メンバー' in author_type:
|
||||
if 'メンバー' in author_type or 'MEMBER' in author_type:
|
||||
isChatSponsor = True
|
||||
if author_type == 'モデレーター':
|
||||
if author_type == 'MODERATOR' or author_type == 'モデレーター':
|
||||
isChatModerator = True
|
||||
return isVerified, isChatOwner, isChatSponsor, isChatModerator
|
||||
|
||||
@@ -76,6 +77,6 @@ class BaseRenderer:
|
||||
return self.renderer.get('id')
|
||||
|
||||
def get_publishedat(self, timestamp):
|
||||
dt = datetime.datetime.fromtimestamp(int(timestamp) / 1000000)
|
||||
return dt.astimezone(pytz.utc).isoformat(
|
||||
dt = datetime.fromtimestamp(int(timestamp) / 1000000)
|
||||
return dt.astimezone(TZ_UTC).isoformat(
|
||||
timespec='milliseconds').replace('+00:00', 'Z')
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'''
|
||||
YouTubeスーパーチャットで使用される通貨の記号とレート検索用の略号の
|
||||
対応表
|
||||
Table of symbols for the currencies used in YouTube Superchat.
|
||||
|
||||
Key:
|
||||
YouTubeスーパーチャットで使用される通貨の記号
|
||||
(アルファベットで終わる場合、0xA0(&npsp)が付く)
|
||||
Currency symbols used in YouTube Super Chat
|
||||
If it ends with an alphabet, it will be followed by 0xA0(&npsp).
|
||||
Value:
|
||||
fxtext: 3文字の通貨略称
|
||||
jptest: 日本語テキスト
|
||||
fxtext: ISO 4217 currency code
|
||||
jptest: japanese text
|
||||
'''
|
||||
symbols = {
|
||||
"$": {"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,5 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from .custom_encoder import CustomEncoder
|
||||
from .renderer.textmessage import LiveChatTextMessageRenderer
|
||||
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
||||
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
||||
@@ -11,25 +13,120 @@ from ... import config
|
||||
logger = config.logger(__name__)
|
||||
|
||||
|
||||
class Chat:
|
||||
def json(self) -> str:
|
||||
return json.dumps(vars(self), ensure_ascii=False, cls=CustomEncoder)
|
||||
|
||||
|
||||
class Chatdata:
|
||||
def __init__(self, chatlist: list, timeout: float):
|
||||
|
||||
def __init__(self, chatlist: list, timeout: float, abs_diff):
|
||||
self.items = chatlist
|
||||
self.interval = timeout
|
||||
self.abs_diff = abs_diff
|
||||
self.itemcount = 0
|
||||
|
||||
def tick(self):
|
||||
if self.interval == 0:
|
||||
'''DEPRECATE
|
||||
Use sync_items()
|
||||
'''
|
||||
if len(self.items) < 1:
|
||||
time.sleep(1)
|
||||
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):
|
||||
if self.interval == 0:
|
||||
'''DEPRECATE
|
||||
Use async_items()
|
||||
'''
|
||||
if len(self.items) < 1:
|
||||
await asyncio.sleep(1)
|
||||
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):
|
||||
def __init__(self):
|
||||
self.first = True
|
||||
self.abs_diff = 0
|
||||
self.renderers = {
|
||||
"liveChatTextMessageRenderer": LiveChatTextMessageRenderer(),
|
||||
"liveChatPaidMessageRenderer": LiveChatPaidMessageRenderer(),
|
||||
"liveChatPaidStickerRenderer": LiveChatPaidStickerRenderer(),
|
||||
"liveChatLegacyPaidMessageRenderer": LiveChatLegacyPaidMessageRenderer(),
|
||||
"liveChatMembershipItemRenderer": LiveChatMembershipItemRenderer()
|
||||
}
|
||||
|
||||
def process(self, chat_components: list):
|
||||
|
||||
chatlist = []
|
||||
@@ -37,8 +134,10 @@ class DefaultProcessor(ChatProcessor):
|
||||
|
||||
if chat_components:
|
||||
for component in chat_components:
|
||||
if component is None:
|
||||
continue
|
||||
timeout += component.get('timeout', 0)
|
||||
chatdata = component.get('chatdata')
|
||||
chatdata = component.get('chatdata') # if from Extractor, chatdata is generator.
|
||||
if chatdata is None:
|
||||
continue
|
||||
for action in chatdata:
|
||||
@@ -46,43 +145,35 @@ class DefaultProcessor(ChatProcessor):
|
||||
continue
|
||||
if action.get('addChatItemAction') is None:
|
||||
continue
|
||||
if action['addChatItemAction'].get('item') is None:
|
||||
item = action['addChatItemAction'].get('item')
|
||||
if item is None:
|
||||
continue
|
||||
|
||||
chat = self._parse(action)
|
||||
chat = self._parse(item)
|
||||
if 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):
|
||||
action = sitem.get("addChatItemAction")
|
||||
if action:
|
||||
item = action.get("item")
|
||||
if item is None:
|
||||
return None
|
||||
chatdata = Chatdata(chatlist, float(timeout), self.abs_diff)
|
||||
|
||||
return chatdata
|
||||
|
||||
def _parse(self, item):
|
||||
try:
|
||||
renderer = self._get_renderer(item)
|
||||
key = list(item.keys())[0]
|
||||
renderer = self.renderers.get(key)
|
||||
if renderer is None:
|
||||
return None
|
||||
|
||||
renderer.setitem(item.get(key), Chat())
|
||||
renderer.settype()
|
||||
renderer.get_snippet()
|
||||
renderer.get_authordetails()
|
||||
rendered_chatobj = renderer.get_chatobj()
|
||||
renderer.clear()
|
||||
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 renderer
|
||||
|
||||
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
|
||||
|
||||
return rendered_chatobj
|
||||
|
||||
@@ -6,89 +6,96 @@ class Author:
|
||||
|
||||
|
||||
class BaseRenderer:
|
||||
def __init__(self, item, chattype):
|
||||
self.renderer = list(item.values())[0]
|
||||
self.chattype = chattype
|
||||
self.author = Author()
|
||||
def setitem(self, item, chat):
|
||||
self.item = item
|
||||
self.chat = chat
|
||||
self.chat.author = Author()
|
||||
|
||||
def settype(self):
|
||||
pass
|
||||
|
||||
def get_snippet(self):
|
||||
self.type = self.chattype
|
||||
self.id = self.renderer.get('id')
|
||||
timestampUsec = int(self.renderer.get("timestampUsec", 0))
|
||||
self.timestamp = int(timestampUsec / 1000)
|
||||
tst = self.renderer.get("timestampText")
|
||||
self.chat.id = self.item.get('id')
|
||||
timestampUsec = int(self.item.get("timestampUsec", 0))
|
||||
self.chat.timestamp = int(timestampUsec / 1000)
|
||||
tst = self.item.get("timestampText")
|
||||
if tst:
|
||||
self.elapsedTime = tst.get("simpleText")
|
||||
self.chat.elapsedTime = tst.get("simpleText")
|
||||
else:
|
||||
self.elapsedTime = ""
|
||||
self.datetime = self.get_datetime(timestampUsec)
|
||||
self.message, self.messageEx = self.get_message(self.renderer)
|
||||
self.id = self.renderer.get('id')
|
||||
self.amountValue = 0.0
|
||||
self.amountString = ""
|
||||
self.currency = ""
|
||||
self.bgColor = 0
|
||||
self.chat.elapsedTime = ""
|
||||
self.chat.datetime = self.get_datetime(timestampUsec)
|
||||
self.chat.message, self.chat.messageEx = self.get_message(self.item)
|
||||
self.chat.id = self.item.get('id')
|
||||
self.chat.amountValue = 0.0
|
||||
self.chat.amountString = ""
|
||||
self.chat.currency = ""
|
||||
self.chat.bgColor = 0
|
||||
|
||||
def get_authordetails(self):
|
||||
self.author.badgeUrl = ""
|
||||
(self.author.isVerified,
|
||||
self.author.isChatOwner,
|
||||
self.author.isChatSponsor,
|
||||
self.author.isChatModerator) = (
|
||||
self.get_badges(self.renderer)
|
||||
self.chat.author.badgeUrl = ""
|
||||
(self.chat.author.isVerified,
|
||||
self.chat.author.isChatOwner,
|
||||
self.chat.author.isChatSponsor,
|
||||
self.chat.author.isChatModerator) = (
|
||||
self.get_badges(self.item)
|
||||
)
|
||||
self.author.channelId = self.renderer.get("authorExternalChannelId")
|
||||
self.author.channelUrl = "http://www.youtube.com/channel/" + self.author.channelId
|
||||
self.author.name = self.renderer["authorName"]["simpleText"]
|
||||
self.author.imageUrl = self.renderer["authorPhoto"]["thumbnails"][1]["url"]
|
||||
self.chat.author.channelId = self.item.get("authorExternalChannelId")
|
||||
self.chat.author.channelUrl = "http://www.youtube.com/channel/" + self.chat.author.channelId
|
||||
self.chat.author.name = self.item["authorName"]["simpleText"]
|
||||
self.chat.author.imageUrl = self.item["authorPhoto"]["thumbnails"][1]["url"]
|
||||
|
||||
def get_message(self, renderer):
|
||||
def get_message(self, item):
|
||||
message = ''
|
||||
message_ex = []
|
||||
if renderer.get("message"):
|
||||
runs = renderer["message"].get("runs")
|
||||
if runs:
|
||||
for r in runs:
|
||||
if r:
|
||||
if r.get('emoji'):
|
||||
message += r['emoji'].get('shortcuts', [''])[0]
|
||||
message_ex.append({
|
||||
'id': r['emoji'].get('emojiId').split('/')[-1],
|
||||
'txt': r['emoji'].get('shortcuts', [''])[0],
|
||||
'url': r['emoji']['image']['thumbnails'][0].get('url')
|
||||
})
|
||||
else:
|
||||
message += r.get('text', '')
|
||||
message_ex.append(r.get('text', ''))
|
||||
runs = item.get("message", {}).get("runs", {})
|
||||
for r in runs:
|
||||
if not hasattr(r, "get"):
|
||||
continue
|
||||
if r.get('emoji'):
|
||||
message += r['emoji'].get('shortcuts', [''])[0]
|
||||
message_ex.append({
|
||||
'id': r['emoji'].get('emojiId').split('/')[-1],
|
||||
'txt': r['emoji'].get('shortcuts', [''])[0],
|
||||
'url': r['emoji']['image']['thumbnails'][0].get('url')
|
||||
})
|
||||
else:
|
||||
message += r.get('text', '')
|
||||
message_ex.append(r.get('text', ''))
|
||||
return message, message_ex
|
||||
|
||||
def get_badges(self, renderer):
|
||||
self.author.type = ''
|
||||
self.chat.author.type = ''
|
||||
isVerified = False
|
||||
isChatOwner = False
|
||||
isChatSponsor = False
|
||||
isChatModerator = False
|
||||
badges = renderer.get("authorBadges")
|
||||
if badges:
|
||||
for badge in badges:
|
||||
if badge["liveChatAuthorBadgeRenderer"].get("icon"):
|
||||
author_type = badge["liveChatAuthorBadgeRenderer"]["icon"]["iconType"]
|
||||
self.author.type = author_type
|
||||
if author_type == 'VERIFIED':
|
||||
isVerified = True
|
||||
if author_type == 'OWNER':
|
||||
isChatOwner = True
|
||||
if author_type == 'MODERATOR':
|
||||
isChatModerator = True
|
||||
if badge["liveChatAuthorBadgeRenderer"].get("customThumbnail"):
|
||||
isChatSponsor = True
|
||||
self.author.type = 'MEMBER'
|
||||
self.get_badgeurl(badge)
|
||||
badges = renderer.get("authorBadges", {})
|
||||
for badge in badges:
|
||||
if badge["liveChatAuthorBadgeRenderer"].get("icon"):
|
||||
author_type = badge["liveChatAuthorBadgeRenderer"]["icon"]["iconType"]
|
||||
self.chat.author.type = author_type
|
||||
if author_type == 'VERIFIED':
|
||||
isVerified = True
|
||||
if author_type == 'OWNER':
|
||||
isChatOwner = True
|
||||
if author_type == 'MODERATOR':
|
||||
isChatModerator = True
|
||||
if badge["liveChatAuthorBadgeRenderer"].get("customThumbnail"):
|
||||
isChatSponsor = True
|
||||
self.chat.author.type = 'MEMBER'
|
||||
self.get_badgeurl(badge)
|
||||
return isVerified, isChatOwner, isChatSponsor, isChatModerator
|
||||
|
||||
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):
|
||||
dt = datetime.fromtimestamp(timestamp / 1000000)
|
||||
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
|
||||
|
||||
@@ -2,14 +2,14 @@ from .base import BaseRenderer
|
||||
|
||||
|
||||
class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
|
||||
def __init__(self, item):
|
||||
super().__init__(item, "newSponsor")
|
||||
def settype(self):
|
||||
self.chat.type = "newSponsor"
|
||||
|
||||
def get_authordetails(self):
|
||||
super().get_authordetails()
|
||||
self.author.isChatSponsor = True
|
||||
self.chat.author.isChatSponsor = True
|
||||
|
||||
def get_message(self, renderer):
|
||||
message = (renderer["eventText"]["runs"][0]["text"]
|
||||
) + ' / ' + (renderer["detailText"]["simpleText"])
|
||||
def get_message(self, item):
|
||||
message = (item["eventText"]["runs"][0]["text"]
|
||||
) + ' / ' + (item["detailText"]["simpleText"])
|
||||
return message, [message]
|
||||
|
||||
@@ -2,14 +2,17 @@ from .base import BaseRenderer
|
||||
|
||||
|
||||
class LiveChatMembershipItemRenderer(BaseRenderer):
|
||||
def __init__(self, item):
|
||||
super().__init__(item, "newSponsor")
|
||||
def settype(self):
|
||||
self.chat.type = "newSponsor"
|
||||
|
||||
def get_authordetails(self):
|
||||
super().get_authordetails()
|
||||
self.author.isChatSponsor = True
|
||||
self.chat.author.isChatSponsor = True
|
||||
|
||||
def get_message(self, renderer):
|
||||
message = ''.join([mes.get("text", "")
|
||||
for mes in renderer["headerSubtext"]["runs"]])
|
||||
def get_message(self, item):
|
||||
try:
|
||||
message = ''.join([mes.get("text", "")
|
||||
for mes in item["headerSubtext"]["runs"]])
|
||||
except KeyError:
|
||||
return "Welcome New Member!", ["Welcome New Member!"]
|
||||
return message, [message]
|
||||
|
||||
@@ -9,23 +9,23 @@ class Colors:
|
||||
|
||||
|
||||
class LiveChatPaidMessageRenderer(BaseRenderer):
|
||||
def __init__(self, item):
|
||||
super().__init__(item, "superChat")
|
||||
def settype(self):
|
||||
self.chat.type = "superChat"
|
||||
|
||||
def get_snippet(self):
|
||||
super().get_snippet()
|
||||
amountDisplayString, symbol, amount = (
|
||||
self.get_amountdata(self.renderer)
|
||||
self.get_amountdata(self.item)
|
||||
)
|
||||
self.amountValue = amount
|
||||
self.amountString = amountDisplayString
|
||||
self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
|
||||
self.chat.amountValue = amount
|
||||
self.chat.amountString = amountDisplayString
|
||||
self.chat.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
|
||||
symbol) else symbol
|
||||
self.bgColor = self.renderer.get("bodyBackgroundColor", 0)
|
||||
self.colors = self.get_colors()
|
||||
self.chat.bgColor = self.item.get("bodyBackgroundColor", 0)
|
||||
self.chat.colors = self.get_colors()
|
||||
|
||||
def get_amountdata(self, renderer):
|
||||
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
|
||||
def get_amountdata(self, item):
|
||||
amountDisplayString = item["purchaseAmountText"]["simpleText"]
|
||||
m = superchat_regex.search(amountDisplayString)
|
||||
if m:
|
||||
symbol = m.group(1)
|
||||
@@ -36,11 +36,12 @@ class LiveChatPaidMessageRenderer(BaseRenderer):
|
||||
return amountDisplayString, symbol, amount
|
||||
|
||||
def get_colors(self):
|
||||
item = self.item
|
||||
colors = Colors()
|
||||
colors.headerBackgroundColor = self.renderer.get("headerBackgroundColor", 0)
|
||||
colors.headerTextColor = self.renderer.get("headerTextColor", 0)
|
||||
colors.bodyBackgroundColor = self.renderer.get("bodyBackgroundColor", 0)
|
||||
colors.bodyTextColor = self.renderer.get("bodyTextColor", 0)
|
||||
colors.timestampColor = self.renderer.get("timestampColor", 0)
|
||||
colors.authorNameTextColor = self.renderer.get("authorNameTextColor", 0)
|
||||
colors.headerBackgroundColor = item.get("headerBackgroundColor", 0)
|
||||
colors.headerTextColor = item.get("headerTextColor", 0)
|
||||
colors.bodyBackgroundColor = item.get("bodyBackgroundColor", 0)
|
||||
colors.bodyTextColor = item.get("bodyTextColor", 0)
|
||||
colors.timestampColor = item.get("timestampColor", 0)
|
||||
colors.authorNameTextColor = item.get("authorNameTextColor", 0)
|
||||
return colors
|
||||
|
||||
@@ -4,30 +4,30 @@ from .base import BaseRenderer
|
||||
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
|
||||
|
||||
|
||||
class Colors:
|
||||
class Colors2:
|
||||
pass
|
||||
|
||||
|
||||
class LiveChatPaidStickerRenderer(BaseRenderer):
|
||||
def __init__(self, item):
|
||||
super().__init__(item, "superSticker")
|
||||
def settype(self):
|
||||
self.chat.type = "superSticker"
|
||||
|
||||
def get_snippet(self):
|
||||
super().get_snippet()
|
||||
amountDisplayString, symbol, amount = (
|
||||
self.get_amountdata(self.renderer)
|
||||
self.get_amountdata(self.item)
|
||||
)
|
||||
self.amountValue = amount
|
||||
self.amountString = amountDisplayString
|
||||
self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
|
||||
self.chat.amountValue = amount
|
||||
self.chat.amountString = amountDisplayString
|
||||
self.chat.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
|
||||
symbol) else symbol
|
||||
self.bgColor = self.renderer.get("backgroundColor", 0)
|
||||
self.sticker = "".join(("https:",
|
||||
self.renderer["sticker"]["thumbnails"][0]["url"]))
|
||||
self.colors = self.get_colors()
|
||||
self.chat.bgColor = self.item.get("backgroundColor", 0)
|
||||
self.chat.sticker = "".join(("https:",
|
||||
self.item["sticker"]["thumbnails"][0]["url"]))
|
||||
self.chat.colors = self.get_colors()
|
||||
|
||||
def get_amountdata(self, renderer):
|
||||
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
|
||||
def get_amountdata(self, item):
|
||||
amountDisplayString = item["purchaseAmountText"]["simpleText"]
|
||||
m = superchat_regex.search(amountDisplayString)
|
||||
if m:
|
||||
symbol = m.group(1)
|
||||
@@ -38,9 +38,10 @@ class LiveChatPaidStickerRenderer(BaseRenderer):
|
||||
return amountDisplayString, symbol, amount
|
||||
|
||||
def get_colors(self):
|
||||
colors = Colors()
|
||||
colors.moneyChipBackgroundColor = self.renderer.get("moneyChipBackgroundColor", 0)
|
||||
colors.moneyChipTextColor = self.renderer.get("moneyChipTextColor", 0)
|
||||
colors.backgroundColor = self.renderer.get("backgroundColor", 0)
|
||||
colors.authorNameTextColor = self.renderer.get("authorNameTextColor", 0)
|
||||
item = self.item
|
||||
colors = Colors2()
|
||||
colors.moneyChipBackgroundColor = item.get("moneyChipBackgroundColor", 0)
|
||||
colors.moneyChipTextColor = item.get("moneyChipTextColor", 0)
|
||||
colors.backgroundColor = item.get("backgroundColor", 0)
|
||||
colors.authorNameTextColor = item.get("authorNameTextColor", 0)
|
||||
return colors
|
||||
|
||||
@@ -2,5 +2,5 @@ from .base import BaseRenderer
|
||||
|
||||
|
||||
class LiveChatTextMessageRenderer(BaseRenderer):
|
||||
def __init__(self, item):
|
||||
super().__init__(item, "textMessage")
|
||||
def settype(self):
|
||||
self.chat.type = "textMessage"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import httpx
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import time
|
||||
from base64 import standard_b64encode
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from .chat_processor import ChatProcessor
|
||||
from .default.processor import DefaultProcessor
|
||||
|
||||
from ..exceptions import UnknownConnectionError
|
||||
import tempfile
|
||||
|
||||
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
|
||||
|
||||
@@ -43,20 +46,24 @@ class HTMLArchiver(ChatProcessor):
|
||||
'''
|
||||
HTMLArchiver saves chat data as HTML table format.
|
||||
'''
|
||||
def __init__(self, save_path):
|
||||
def __init__(self, save_path, callback=None):
|
||||
super().__init__()
|
||||
self.client = httpx.Client(http2=True)
|
||||
self.save_path = self._checkpath(save_path)
|
||||
self.processor = DefaultProcessor()
|
||||
self.emoji_table = {} # tuble 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.emoji_table = {} # dict for custom emojis. key: emoji_id, value: base64 encoded image binary.
|
||||
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):
|
||||
splitter = os.path.splitext(os.path.basename(filepath))
|
||||
body = splitter[0]
|
||||
extention = splitter[1]
|
||||
newpath = filepath
|
||||
counter = 0
|
||||
counter = 1
|
||||
while os.path.exists(newpath):
|
||||
match = re.search(PATTERN, body)
|
||||
if match:
|
||||
@@ -76,21 +83,26 @@ class HTMLArchiver(ChatProcessor):
|
||||
save_path : str :
|
||||
Actual save path of file.
|
||||
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:
|
||||
return
|
||||
self.body.extend(
|
||||
(self._parse_html_line((
|
||||
c.datetime,
|
||||
c.elapsedTime,
|
||||
c.author.name,
|
||||
self._parse_message(c.messageEx),
|
||||
c.amountString,
|
||||
c.author.type,
|
||||
c.author.channelId)
|
||||
) for c in self.processor.process(chat_components).items)
|
||||
)
|
||||
return self.save_path, self.counter
|
||||
for c in self.processor.process(chat_components).items:
|
||||
self.tmp_fp.write(
|
||||
self._parse_html_line((
|
||||
c.datetime,
|
||||
c.elapsedTime,
|
||||
c.author.name,
|
||||
self._parse_message(c.messageEx),
|
||||
c.amountString,
|
||||
c.author.type,
|
||||
c.author.channelId)
|
||||
)
|
||||
)
|
||||
if self.callback:
|
||||
self.callback(None, 1)
|
||||
self.counter += 1
|
||||
return self.save_path, self.counter
|
||||
|
||||
def _parse_html_line(self, raw_line):
|
||||
return ''.join(('<tr>',
|
||||
@@ -108,13 +120,23 @@ class HTMLArchiver(ChatProcessor):
|
||||
for item in message_items)
|
||||
|
||||
def _encode_img(self, url):
|
||||
resp = requests.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()
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
def _stylecode(self, name, code, width, height):
|
||||
@@ -125,13 +147,24 @@ class HTMLArchiver(ChatProcessor):
|
||||
def _create_styles(self):
|
||||
return '\n'.join(('<style type="text/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()),
|
||||
'</style>\n'))
|
||||
|
||||
def finalize(self):
|
||||
self.header.extend([self._create_styles(), '</head>\n'])
|
||||
self.body.extend(['</table>\n</body>'])
|
||||
with open(self.save_path, mode='a', encoding='utf-8') as f:
|
||||
f.writelines(self.header)
|
||||
f.writelines(self.body)
|
||||
if self.tmp_fp:
|
||||
self.tmp_fp.flush()
|
||||
self.tmp_fp = None
|
||||
with open(self.save_path, mode='w', encoding='utf-8') as outfile:
|
||||
# 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)
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import aiohttp
|
||||
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
|
||||
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 aiohttp.ClientSession() 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:
|
||||
async with session.get(url, headers=headers) as resp:
|
||||
text = await resp.text()
|
||||
next_continuation, actions = parser.parse(json.loads(text))
|
||||
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 aiohttp.ClientSession() 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:
|
||||
async with session.get(url, headers=config.headers) as resp:
|
||||
chat_json = await resp.text()
|
||||
continuation, actions = parser.parse(json.loads(chat_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,92 +0,0 @@
|
||||
from . block import Block
|
||||
from . patch import fill, split
|
||||
from ... paramgen import arcparam
|
||||
|
||||
|
||||
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) -> (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,141 +0,0 @@
|
||||
|
||||
import aiohttp
|
||||
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 aiohttp.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 aiohttp.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,155 +0,0 @@
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
from .. import config
|
||||
from ..exceptions import InvalidVideoIdException
|
||||
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 = requests.get(url, headers=headers)
|
||||
resp.raise_for_status()
|
||||
return resp.text
|
||||
|
||||
def _parse(self, text):
|
||||
result = re.search(pattern, text)
|
||||
res = json.loads(result.group(1)[:-1])
|
||||
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,18 +1,120 @@
|
||||
import requests
|
||||
import json
|
||||
import datetime
|
||||
import httpx
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
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})\\\"")
|
||||
|
||||
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):
|
||||
_session = requests.Session()
|
||||
_session = httpx.Client(http2=True)
|
||||
html = _session.get(url, headers=config.headers)
|
||||
with open(str(datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
|
||||
) + 'test.json', mode='w', encoding='utf-8') as f:
|
||||
json.dump(html.json(), f, ensure_ascii=False)
|
||||
|
||||
|
||||
def save(data, filename, extention):
|
||||
with open(filename + "_" + (datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')) + extention,
|
||||
mode='w', encoding='utf-8') as f:
|
||||
def save(data, filename, extention) -> str:
|
||||
save_filename = filename + "_" + \
|
||||
(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)
|
||||
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)
|
||||
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)
|
||||
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,5 +1 @@
|
||||
aiohttp
|
||||
protobuf
|
||||
pytz
|
||||
requests
|
||||
urllib3
|
||||
httpx[http2]==0.16.1
|
||||
@@ -1,5 +1,2 @@
|
||||
aioresponses
|
||||
mock
|
||||
mocker
|
||||
pytest
|
||||
pytest-mock
|
||||
pytest-mock
|
||||
pytest-httpx
|
||||
|
||||
5
setup.py
5
setup.py
@@ -55,11 +55,6 @@ setup(
|
||||
'License :: OSI Approved :: MIT License',
|
||||
],
|
||||
description="a python library for fetching youtube live chat.",
|
||||
entry_points=
|
||||
'''
|
||||
[console_scripts]
|
||||
pytchat=pytchat.cli:main
|
||||
''',
|
||||
install_requires=_requirements(),
|
||||
keywords='youtube livechat asyncio',
|
||||
license=license,
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import json
|
||||
import requests
|
||||
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 = requests.Session().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 requests
|
||||
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 = requests.Session().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"
|
||||
@@ -6,50 +6,55 @@ parse = SuperchatCalculator()._parse
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def load_chatdata(filepath):
|
||||
parser = Parser(is_replay=True)
|
||||
#print(json.loads(_open_file(filepath)))
|
||||
contents = parser.get_contents( json.loads(_open_file(filepath)))
|
||||
# print(json.loads(_open_file(filepath)))
|
||||
contents = parser.get_contents(json.loads(_open_file(filepath)))[0]
|
||||
return parser.parse(contents)[1]
|
||||
|
||||
|
||||
|
||||
def test_parse_1():
|
||||
renderer ={"purchaseAmountText":{"simpleText":"¥2,000"}}
|
||||
symbol ,amount = parse(renderer)
|
||||
renderer = {"purchaseAmountText": {"simpleText": "¥2,000"}}
|
||||
symbol, amount = parse(renderer)
|
||||
assert symbol == '¥'
|
||||
assert amount == 2000.0
|
||||
|
||||
|
||||
def test_parse_2():
|
||||
renderer ={"purchaseAmountText":{"simpleText":"ABC\x0a200"}}
|
||||
symbol ,amount = parse(renderer)
|
||||
renderer = {"purchaseAmountText": {"simpleText": "ABC\x0a200"}}
|
||||
symbol, amount = parse(renderer)
|
||||
assert symbol == 'ABC\x0a'
|
||||
assert amount == 200.0
|
||||
|
||||
|
||||
def test_process_0():
|
||||
"""
|
||||
parse superchat data
|
||||
"""
|
||||
"""
|
||||
chat_component = {
|
||||
'video_id':'',
|
||||
'timeout':10,
|
||||
'chatdata':load_chatdata(r"tests/testdata/calculator/superchat_0.json")
|
||||
'video_id': '',
|
||||
'timeout': 10,
|
||||
'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():
|
||||
"""
|
||||
parse no superchat data
|
||||
"""
|
||||
chat_component = {
|
||||
'video_id':'',
|
||||
'timeout':10,
|
||||
'chatdata':load_chatdata(r"tests/testdata/calculator/text_only.json")
|
||||
'video_id': '',
|
||||
'timeout': 10,
|
||||
'chatdata': load_chatdata(r"tests/testdata/calculator/text_only.json")
|
||||
}
|
||||
assert SuperchatCalculator().process([chat_component])=={}
|
||||
assert SuperchatCalculator().process([chat_component]) == {}
|
||||
|
||||
|
||||
def test_process_2():
|
||||
"""
|
||||
@@ -57,12 +62,11 @@ def test_process_2():
|
||||
"""
|
||||
try:
|
||||
chat_component = {
|
||||
'video_id':'',
|
||||
'timeout':10,
|
||||
'chatdata':load_chatdata(r"tests/testdata/calculator/replay_end.json")
|
||||
'video_id': '',
|
||||
'timeout': 10,
|
||||
'chatdata': load_chatdata(r"tests/testdata/calculator/replay_end.json")
|
||||
}
|
||||
assert False
|
||||
SuperchatCalculator().process([chat_component])
|
||||
except ChatParseException:
|
||||
assert True
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ def test_textmessage(mocker):
|
||||
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -51,7 +51,7 @@ def test_newsponcer(mocker):
|
||||
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -88,7 +88,7 @@ def test_newsponcer_rev(mocker):
|
||||
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -125,7 +125,7 @@ def test_superchat(mocker):
|
||||
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -164,7 +164,7 @@ def test_unregistered_currency(mocker):
|
||||
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pytchat.parser.live import Parser
|
||||
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):
|
||||
'''text message'''
|
||||
processor = DefaultProcessor()
|
||||
parser = Parser(is_replay=False)
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -17,11 +26,10 @@ def test_textmessage(mocker):
|
||||
}
|
||||
|
||||
ret = processor.process([data]).items[0]
|
||||
assert ret.chattype == "textMessage"
|
||||
assert ret.id == "dummy_id"
|
||||
assert ret.message == "dummy_message"
|
||||
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.channelId == "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)
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -47,13 +55,12 @@ def test_textmessage_replay_member(mocker):
|
||||
}
|
||||
|
||||
ret = processor.process([data]).items[0]
|
||||
assert ret.chattype == "textMessage"
|
||||
assert ret.type == "textMessage"
|
||||
assert ret.id == "dummy_id"
|
||||
assert ret.message == "dummy_message"
|
||||
assert ret.messageEx == ["dummy_message"]
|
||||
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.author.name == "author_name"
|
||||
assert ret.author.channelId == "author_channel_id"
|
||||
@@ -72,7 +79,7 @@ def test_superchat(mocker):
|
||||
parser = Parser(is_replay=False)
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -80,14 +87,12 @@ def test_superchat(mocker):
|
||||
}
|
||||
|
||||
ret = processor.process([data]).items[0]
|
||||
print(json.dumps(chatdata, ensure_ascii=False))
|
||||
assert ret.chattype == "superChat"
|
||||
assert ret.type == "superChat"
|
||||
assert ret.id == "dummy_id"
|
||||
assert ret.message == "dummy_message"
|
||||
assert ret.messageEx == ["dummy_message"]
|
||||
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.amountValue == 800
|
||||
assert ret.amountString == "¥800"
|
||||
@@ -116,7 +121,7 @@ def test_supersticker(mocker):
|
||||
parser = Parser(is_replay=False)
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -124,14 +129,12 @@ def test_supersticker(mocker):
|
||||
}
|
||||
|
||||
ret = processor.process([data]).items[0]
|
||||
print(json.dumps(chatdata, ensure_ascii=False))
|
||||
assert ret.chattype == "superSticker"
|
||||
assert ret.type == "superSticker"
|
||||
assert ret.id == "dummy_id"
|
||||
assert ret.message == ""
|
||||
assert ret.messageEx == []
|
||||
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.amountValue == 200
|
||||
assert ret.amountString == "¥200"
|
||||
@@ -159,7 +162,7 @@ def test_sponsor(mocker):
|
||||
parser = Parser(is_replay=False)
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -167,14 +170,12 @@ def test_sponsor(mocker):
|
||||
}
|
||||
|
||||
ret = processor.process([data]).items[0]
|
||||
print(json.dumps(chatdata, ensure_ascii=False))
|
||||
assert ret.chattype == "newSponsor"
|
||||
assert ret.type == "newSponsor"
|
||||
assert ret.id == "dummy_id"
|
||||
assert ret.message == "新規メンバー"
|
||||
assert ret.messageEx == ["新規メンバー"]
|
||||
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.bgColor == 0
|
||||
assert ret.author.name == "author_name"
|
||||
@@ -194,7 +195,7 @@ def test_sponsor_legacy(mocker):
|
||||
parser = Parser(is_replay=False)
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 7,
|
||||
@@ -202,14 +203,12 @@ def test_sponsor_legacy(mocker):
|
||||
}
|
||||
|
||||
ret = processor.process([data]).items[0]
|
||||
print(json.dumps(chatdata, ensure_ascii=False))
|
||||
assert ret.chattype == "newSponsor"
|
||||
assert ret.type == "newSponsor"
|
||||
assert ret.id == "dummy_id"
|
||||
assert ret.message == "新規メンバー / ようこそ、author_name!"
|
||||
assert ret.messageEx == ["新規メンバー / ようこそ、author_name!"]
|
||||
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.bgColor == 0
|
||||
assert ret.author.name == "author_name"
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
from pytchat.tool.extract import parser
|
||||
import sys
|
||||
import time
|
||||
from aioresponses import aioresponses
|
||||
from concurrent.futures import CancelledError
|
||||
from pytchat.tool.extract import asyncdl
|
||||
|
||||
def _open_file(path):
|
||||
with open(path,mode ='r',encoding = 'utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def test_asyncdl_split():
|
||||
|
||||
ret = asyncdl._split(0,1000,1)
|
||||
assert ret == [0]
|
||||
|
||||
ret = asyncdl._split(1000,1000,10)
|
||||
assert ret == [1000]
|
||||
|
||||
ret = asyncdl._split(0,1000,5)
|
||||
assert ret == [0,200,400,600,800]
|
||||
|
||||
ret = asyncdl._split(10.5, 700.3, 5)
|
||||
assert ret == [10, 148, 286, 424, 562]
|
||||
|
||||
|
||||
ret = asyncdl._split(0,500,5)
|
||||
assert ret == [0,125,250,375]
|
||||
|
||||
ret = asyncdl._split(0,500,500)
|
||||
assert ret == [0,125,250,375]
|
||||
|
||||
ret = asyncdl._split(-1,1000,5)
|
||||
assert ret == [-1, 199, 399, 599, 799]
|
||||
|
||||
"""invalid argument order"""
|
||||
try:
|
||||
ret = asyncdl._split(500,0,5)
|
||||
assert False
|
||||
except ValueError:
|
||||
assert True
|
||||
|
||||
"""invalid count"""
|
||||
try:
|
||||
ret = asyncdl._split(0,500,-1)
|
||||
assert False
|
||||
except ValueError:
|
||||
assert True
|
||||
|
||||
try:
|
||||
ret = asyncdl._split(0,500,0)
|
||||
assert False
|
||||
except ValueError:
|
||||
assert True
|
||||
|
||||
"""invalid argument type"""
|
||||
try:
|
||||
ret = asyncdl._split(0,5000,5.2)
|
||||
assert False
|
||||
except ValueError:
|
||||
assert True
|
||||
|
||||
try:
|
||||
ret = asyncdl._split(0,5000,"test")
|
||||
assert False
|
||||
except ValueError:
|
||||
assert True
|
||||
|
||||
try:
|
||||
ret = asyncdl._split([0,1],5000,5)
|
||||
assert False
|
||||
except ValueError:
|
||||
assert True
|
||||
@@ -1,128 +0,0 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
import os, sys
|
||||
import time
|
||||
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,238 +0,0 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
import os, sys
|
||||
import time
|
||||
from aioresponses import aioresponses
|
||||
from pytchat.tool.extract import duplcheck
|
||||
from pytchat.tool.extract import parser
|
||||
from pytchat.tool.extract.block import Block
|
||||
from pytchat.tool.extract.patch import Patch, fill, split, set_patch
|
||||
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 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 == False
|
||||
assert child.during_split == 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 == False
|
||||
assert child.during_split == 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 == False
|
||||
assert child.during_split == 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 == False
|
||||
assert child.during_split == 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
|
||||
|
||||
VALID_TEST_PATTERNS = (
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import json
|
||||
from aioresponses import aioresponses
|
||||
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()
|
||||
|
||||
|
||||
@aioresponses()
|
||||
def test_Async(*mock):
|
||||
vid = '__test_id__'
|
||||
_text = _open_file('tests/testdata/paramgen_firstread.json')
|
||||
_text = json.loads(_text)
|
||||
mock[0].get(
|
||||
f"https://www.youtube.com/live_chat?v={vid}&is_popout=1", status=200, body=_text)
|
||||
try:
|
||||
chat = LiveChatAsync(video_id='__test_id__')
|
||||
assert chat.is_alive()
|
||||
chat.terminate()
|
||||
assert not chat.is_alive()
|
||||
except ResponseContextError:
|
||||
assert not chat.is_alive()
|
||||
|
||||
|
||||
def test_MultiThread(mocker):
|
||||
_text = _open_file('tests/testdata/paramgen_firstread.json')
|
||||
_text = json.loads(_text)
|
||||
responseMock = mocker.Mock()
|
||||
responseMock.status_code = 200
|
||||
responseMock.text = _text
|
||||
mocker.patch('requests.Session.get').return_value = responseMock
|
||||
try:
|
||||
chat = LiveChatAsync(video_id='__test_id__')
|
||||
assert chat.is_alive()
|
||||
chat.terminate()
|
||||
assert not chat.is_alive()
|
||||
except ResponseContextError:
|
||||
chat.terminate()
|
||||
assert not chat.is_alive()
|
||||
@@ -1,140 +0,0 @@
|
||||
import asyncio
|
||||
import re
|
||||
from aioresponses import aioresponses
|
||||
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()
|
||||
|
||||
|
||||
@aioresponses()
|
||||
def test_async_live_stream(*mock):
|
||||
|
||||
async def test_loop(*mock):
|
||||
pattern = re.compile(
|
||||
r'^https://www.youtube.com/live_chat/get_live_chat\?continuation=.*$')
|
||||
_text = _open_file('tests/testdata/test_stream.json')
|
||||
mock[0].get(pattern, status=200, body=_text)
|
||||
chat = LiveChatAsync(video_id='__test_id__', processor=DummyProcessor())
|
||||
chats = await 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"
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(test_loop(*mock))
|
||||
except CancelledError:
|
||||
assert True
|
||||
|
||||
|
||||
@aioresponses()
|
||||
def test_async_replay_stream(*mock):
|
||||
|
||||
async def test_loop(*mock):
|
||||
pattern_live = re.compile(
|
||||
r'^https://www.youtube.com/live_chat/get_live_chat\?continuation=.*$')
|
||||
pattern_replay = re.compile(
|
||||
r'^https://www.youtube.com/live_chat_replay/get_live_chat_replay\?continuation=.*$')
|
||||
# empty livechat -> switch to fetch replaychat
|
||||
_text_live = _open_file('tests/testdata/finished_live.json')
|
||||
_text_replay = _open_file('tests/testdata/chatreplay.json')
|
||||
mock[0].get(pattern_live, status=200, body=_text_live)
|
||||
mock[0].get(pattern_replay, status=200, body=_text_replay)
|
||||
|
||||
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(*mock))
|
||||
except CancelledError:
|
||||
assert True
|
||||
|
||||
|
||||
@aioresponses()
|
||||
def test_async_force_replay(*mock):
|
||||
|
||||
async def test_loop(*mock):
|
||||
pattern_live = re.compile(
|
||||
r'^https://www.youtube.com/live_chat/get_live_chat\?continuation=.*$')
|
||||
pattern_replay = re.compile(
|
||||
r'^https://www.youtube.com/live_chat_replay/get_live_chat_replay\?continuation=.*$')
|
||||
# valid live data, but force_replay = True
|
||||
_text_live = _open_file('tests/testdata/test_stream.json')
|
||||
# valid replay data
|
||||
_text_replay = _open_file('tests/testdata/chatreplay.json')
|
||||
|
||||
mock[0].get(pattern_live, status=200, body=_text_live)
|
||||
mock[0].get(pattern_replay, status=200, body=_text_replay)
|
||||
# force replay
|
||||
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(*mock))
|
||||
except CancelledError:
|
||||
assert True
|
||||
|
||||
|
||||
def test_multithread_live_stream(mocker):
|
||||
|
||||
_text = _open_file('tests/testdata/test_stream.json')
|
||||
responseMock = mocker.Mock()
|
||||
responseMock.status_code = 200
|
||||
responseMock.text = _text
|
||||
mocker.patch(
|
||||
'requests.Session.get').return_value.__enter__.return_value = responseMock
|
||||
|
||||
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
|
||||
@@ -1,45 +1,34 @@
|
||||
from pytchat.parser.live import Parser
|
||||
import json
|
||||
from aioresponses import aioresponses
|
||||
from pytchat.exceptions import NoContents
|
||||
|
||||
|
||||
parser = Parser(is_replay=False)
|
||||
|
||||
|
||||
def _open_file(path):
|
||||
with open(path, mode='r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
parser = Parser(is_replay=False)
|
||||
|
||||
|
||||
@aioresponses()
|
||||
def test_finishedlive(*mock):
|
||||
'''配信が終了した動画を正しく処理できるか'''
|
||||
|
||||
_text = _open_file('tests/testdata/finished_live.json')
|
||||
_text = json.loads(_text)
|
||||
|
||||
try:
|
||||
parser.parse(parser.get_contents(_text))
|
||||
parser.parse(parser.get_contents(_text)[0])
|
||||
assert False
|
||||
except NoContents:
|
||||
assert True
|
||||
|
||||
|
||||
@aioresponses()
|
||||
def test_parsejson(*mock):
|
||||
'''jsonを正常にパースできるか'''
|
||||
|
||||
_text = _open_file('tests/testdata/paramgen_firstread.json')
|
||||
_text = json.loads(_text)
|
||||
|
||||
try:
|
||||
parser.parse(parser.get_contents(_text))
|
||||
jsn = _text
|
||||
timeout = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["timeoutMs"]
|
||||
continuation = jsn["response"]["continuationContents"]["liveChatContinuation"][
|
||||
"continuations"][0]["timedContinuationData"]["continuation"]
|
||||
assert timeout == 5035
|
||||
assert continuation == "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%3D"
|
||||
s, _ = parser.parse(parser.get_contents(_text)[0])
|
||||
assert s['timeoutMs'] == 5035
|
||||
assert s['continuation'] == "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%3D"
|
||||
except Exception:
|
||||
assert False
|
||||
|
||||
@@ -15,7 +15,7 @@ def test_speed_1(mocker):
|
||||
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 10,
|
||||
@@ -32,7 +32,7 @@ def test_speed_2(mocker):
|
||||
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 10,
|
||||
@@ -49,7 +49,7 @@ def test_speed_3(mocker):
|
||||
|
||||
_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 = {
|
||||
"video_id": "",
|
||||
"timeout": 10,
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
from pytchat.tool.videoinfo import VideoInfo
|
||||
from pytchat.exceptions import InvalidVideoIdException
|
||||
|
||||
|
||||
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('requests.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
|
||||
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": {
|
||||
"webResponseContextExtensionData": ""
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"replayChatItemAction": {
|
||||
"actions": [
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatTextMessageRenderer": {
|
||||
"message": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "dummy_message"
|
||||
}
|
||||
]
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
|
||||
"responseContext": {
|
||||
"serviceTrackingParams": [
|
||||
{
|
||||
@@ -1805,7 +1805,7 @@
|
||||
}
|
||||
},
|
||||
"trackingParams": "CAAQ0b4BIhMI1LbVw_aa5QIV2cxMAh2AtAj8"
|
||||
},
|
||||
,
|
||||
"url": "\/live_chat\/get_live_chat?continuation=0ofMyAOBAhrKAUNrd1NJUW9ZVlVOdVVsRlpTRlJ1VWt4VFJqQmpURXAzVFc1bFpFTm5FZ1V2YkdsMlpTb25DaGhWUTI1U1VWbElWRzVTVEZOR01HTk1TbmROYm1Wa1EyY1NDMDFETTNkVlNpMUNXRTVGR2tPcXVjRzlBVDBLTzJoMGRIQnpPaTh2ZDNkM0xubHZkWFIxWW1VdVkyOXRMMnhwZG1WZlkyaGhkRDkyUFUxRE0zZFZTaTFDV0U1RkptbHpYM0J2Y0c5MWREMHhJQUklM0QwAUopCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoAUPOaw8P2muUCWANoAYIBAggB",
|
||||
"csn": "PvujXbH0OIazqQHXgJ64DQ",
|
||||
"endpoint": {
|
||||
|
||||
541
tests/testdata/compatible/superchat.json
vendored
541
tests/testdata/compatible/superchat.json
vendored
@@ -1,282 +1,281 @@
|
||||
{
|
||||
"xsrf_token": "QUFFLUhqbWVqWVRTUjhBcGRkNWR5Q2F3VWhxcTd0RkF1UXxBQ3Jtc0trMThFVGVVNTFnWmZucnkwNlJJMEZ2bndUS1I3b2dpLUtTNE92dEgwX3Y0MkNZU2NXdGY1QTMtX09BUGphYUpDQlc1dFhiTm9jLS1sQXVCNHpsdllqcm1id0t1RFBCM3A1b2o3OGt0Yjd6TE5wcmxBbjNyQktjc1lWZ3hjM1RuYk83YkQ0VVN3MGUybjAwSE90SS1YNkxvMUV5YVE=",
|
||||
"timing": {
|
||||
"info": {
|
||||
"st": 148
|
||||
}
|
||||
"xsrf_token": "QUFFLUhqbWVqWVRTUjhBcGRkNWR5Q2F3VWhxcTd0RkF1UXxBQ3Jtc0trMThFVGVVNTFnWmZucnkwNlJJMEZ2bndUS1I3b2dpLUtTNE92dEgwX3Y0MkNZU2NXdGY1QTMtX09BUGphYUpDQlc1dFhiTm9jLS1sQXVCNHpsdllqcm1id0t1RFBCM3A1b2o3OGt0Yjd6TE5wcmxBbjNyQktjc1lWZ3hjM1RuYk83YkQ0VVN3MGUybjAwSE90SS1YNkxvMUV5YVE=",
|
||||
"timing": {
|
||||
"info": {
|
||||
"st": 148
|
||||
}
|
||||
},
|
||||
"endpoint": {
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
||||
}
|
||||
},
|
||||
"endpoint": {
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/live_chat/get_live_chat?continuation=0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCja2bHMpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DXxuTMpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABxM3xzKT75AI%253D"
|
||||
}
|
||||
},
|
||||
"urlEndpoint": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatPaidMessageRenderer": {
|
||||
"id": "ChwKGkNOblpoTXFrLS1RQ0ZRSU9ZQW9kclhrRXNn",
|
||||
"timestampUsec": "1569940638420061",
|
||||
"authorName": {
|
||||
"simpleText": "九十九 万"
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
"authorPhoto": {
|
||||
"thumbnails": [
|
||||
{
|
||||
"timedContinuationData": {
|
||||
"timeoutMs": 8860,
|
||||
"continuation": "0ofMyAPiARp8Q2c4S0RRb0xPREJQZW5SS2FIVTNOemdhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5T0RCUGVuUkthSFUzTnpnbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCid_ejQpPvkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQwJKPoqT75AJYA1DF-ovRpPvkAliL_fvZoPvkAmgBggEECAEQAIgBAKABtNic0aT75AI%3D"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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": "コメントの操作"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"url": "https://yt3.ggpht.com/-FoBvzoKhbZs/AAAAAAAAAAI/AAAAAAAAAAA/oyb-UCYFbfA/s32-c-k-no-mo-rj-c0xffffff/photo.jpg",
|
||||
"width": 32,
|
||||
"height": 32
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
"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": {
|
||||
"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=",
|
||||
"timing": {
|
||||
"info": {
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"xsrf_token": "QUFFLUhqbWVqWVRTUjhBcGRkNWR5Q2F3VWhxcTd0RkF1UXxBQ3Jtc0trMThFVGVVNTFnWmZucnkwNlJJMEZ2bndUS1I3b2dpLUtTNE92dEgwX3Y0MkNZU2NXdGY1QTMtX09BUGphYUpDQlc1dFhiTm9jLS1sQXVCNHpsdllqcm1id0t1RFBCM3A1b2o3OGt0Yjd6TE5wcmxBbjNyQktjc1lWZ3hjM1RuYk83YkQ0VVN3MGUybjAwSE90SS1YNkxvMUV5YVE=",
|
||||
"timing": {
|
||||
"info": {
|
||||
"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",
|
||||
"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": {
|
||||
"serviceTrackingParams": [
|
||||
{
|
||||
"service": "CSI",
|
||||
"params": [
|
||||
{
|
||||
"key": "GetLiveChat_rid",
|
||||
"value": "0x3eff0db28fc39bbe"
|
||||
},
|
||||
{
|
||||
"key": "c",
|
||||
"value": "WEB"
|
||||
},
|
||||
{
|
||||
"key": "cver",
|
||||
"value": "2.20190920.05.01"
|
||||
},
|
||||
{
|
||||
"key": "yt_li",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "GFEEDBACK",
|
||||
"params": [
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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.20190920"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.changelist",
|
||||
"value": "270169303"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.experiments.source_version",
|
||||
"value": "270377311"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.label",
|
||||
"value": "youtube.ytfe.innertube_20190919_5_RC1"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.timestamp",
|
||||
"value": "1568942548"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.variants.checksum",
|
||||
"value": "392d499f55b5e2c240adde58886a8143"
|
||||
},
|
||||
{
|
||||
"key": "innertube.run.job",
|
||||
"value": "ytfe-innertube-replica-only.ytfe"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"webResponseContextExtensionData": {
|
||||
"ytConfigData": {
|
||||
"csn": "n96GXabRGouFlQTigY2YDg",
|
||||
"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
|
||||
}
|
||||
"responseContext": {
|
||||
"serviceTrackingParams": [
|
||||
{
|
||||
"service": "CSI",
|
||||
"params": [
|
||||
{
|
||||
"key": "GetLiveChat_rid",
|
||||
"value": "0x3eff0db28fc39bbe"
|
||||
},
|
||||
{
|
||||
"key": "c",
|
||||
"value": "WEB"
|
||||
},
|
||||
{
|
||||
"key": "cver",
|
||||
"value": "2.20190920.05.01"
|
||||
},
|
||||
{
|
||||
"key": "yt_li",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "GFEEDBACK",
|
||||
"params": [
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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.20190920"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.changelist",
|
||||
"value": "270169303"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.experiments.source_version",
|
||||
"value": "270377311"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.label",
|
||||
"value": "youtube.ytfe.innertube_20190919_5_RC1"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.timestamp",
|
||||
"value": "1568942548"
|
||||
},
|
||||
{
|
||||
"key": "innertube.build.variants.checksum",
|
||||
"value": "392d499f55b5e2c240adde58886a8143"
|
||||
},
|
||||
{
|
||||
"key": "innertube.run.job",
|
||||
"value": "ytfe-innertube-replica-only.ytfe"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"webResponseContextExtensionData": {
|
||||
"ytConfigData": {
|
||||
"csn": "n96GXabRGouFlQTigY2YDg",
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
166
tests/testdata/default/newSponsor_current.json
vendored
166
tests/testdata/default/newSponsor_current.json
vendored
@@ -1,100 +1,98 @@
|
||||
{
|
||||
"response": {
|
||||
"responseContext": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
"responseContext": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatMembershipItemRenderer": {
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"authorExternalChannelId": "author_channel_id",
|
||||
"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": [
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatMembershipItemRenderer": {
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"authorExternalChannelId": "author_channel_id",
|
||||
"headerSubtext": {
|
||||
"runs": [
|
||||
{
|
||||
"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": "新規メンバー"
|
||||
"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": {
|
||||
"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": {
|
||||
"webCommandMetadata": {
|
||||
"ignoreNavigation": true
|
||||
}
|
||||
},
|
||||
"liveChatItemContextMenuEndpoint": {
|
||||
"params": "___params___"
|
||||
}
|
||||
],
|
||||
"contextMenuEndpoint": {
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"ignoreNavigation": true
|
||||
}
|
||||
},
|
||||
"contextMenuAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "コメントの操作"
|
||||
}
|
||||
"liveChatItemContextMenuEndpoint": {
|
||||
"params": "___params___"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
"responseContext": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatLegacyPaidMessageRenderer": {
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"eventText": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "新規メンバー"
|
||||
}
|
||||
]
|
||||
},
|
||||
"detailText": {
|
||||
"simpleText": "ようこそ、author_name!"
|
||||
},
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"authorExternalChannelId": "author_channel_id",
|
||||
"contextMenuEndpoint": {
|
||||
"clickTrackingParams": "___clickTrackingParams___",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"ignoreNavigation": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatLegacyPaidMessageRenderer": {
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"eventText": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "新規メンバー"
|
||||
}
|
||||
]
|
||||
},
|
||||
"detailText": {
|
||||
"simpleText": "ようこそ、author_name!"
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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": {
|
||||
"accessibilityData": {
|
||||
"label": "コメントの操作"
|
||||
}
|
||||
"liveChatItemContextMenuEndpoint": {
|
||||
"params": "___params___"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"webResponseContextExtensionData": "data"
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"liveChatReplayContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
"responseContext": {
|
||||
"webResponseContextExtensionData": "data"
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"liveChatReplayContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
],
|
||||
"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/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||
"width": 64,
|
||||
"height": 64
|
||||
}
|
||||
]
|
||||
},
|
||||
"contextMenuEndpoint": {
|
||||
"clickTrackingParams": "___clickTrackingParams___",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"ignoreNavigation": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"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
|
||||
},
|
||||
"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",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"authorBadges": [
|
||||
{
|
||||
"liveChatAuthorBadgeRenderer": {
|
||||
"customThumbnail": {
|
||||
"thumbnails": [
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/X=s16-c-k"
|
||||
},
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/X=s32-c-k"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tooltip": "メンバー(1 か月)",
|
||||
"accessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "メンバー(1 か月)"
|
||||
"liveChatItemContextMenuEndpoint": {
|
||||
"params": "___params___"
|
||||
}
|
||||
},
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"authorBadges": [
|
||||
{
|
||||
"liveChatAuthorBadgeRenderer": {
|
||||
"customThumbnail": {
|
||||
"thumbnails": [
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/X=s16-c-k"
|
||||
},
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/X=s32-c-k"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
"responseContext": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatPaidMessageRenderer": {
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatPaidMessageRenderer": {
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"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
|
||||
},
|
||||
"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,
|
||||
"contextMenuAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "コメントの操作"
|
||||
}
|
||||
"liveChatItemContextMenuEndpoint": {
|
||||
"params": "___params___"
|
||||
}
|
||||
},
|
||||
"timestampColor": 2147483648,
|
||||
"contextMenuAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "コメントの操作"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"addLiveChatTickerItemAction": {
|
||||
"item": {
|
||||
"liveChatTickerPaidMessageItemRenderer": {
|
||||
"id": "dummy_id",
|
||||
"amount": {
|
||||
"simpleText": "¥846"
|
||||
},
|
||||
"amountTextColor": 4278190080,
|
||||
"startBackgroundColor": 4280150454,
|
||||
"endBackgroundColor": 4278239141,
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"durationSec": 120,
|
||||
"showItemEndpoint": {
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"ignoreNavigation": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"addLiveChatTickerItemAction": {
|
||||
"item": {
|
||||
"liveChatTickerPaidMessageItemRenderer": {
|
||||
"id": "dummy_id",
|
||||
"amount": {
|
||||
"simpleText": "¥846"
|
||||
},
|
||||
"amountTextColor": 4278190080,
|
||||
"startBackgroundColor": 4280150454,
|
||||
"endBackgroundColor": 4278239141,
|
||||
"authorPhoto": {
|
||||
"thumbnails": [
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||
"width": 32,
|
||||
"height": 32
|
||||
},
|
||||
"showLiveChatItemEndpoint": {
|
||||
"renderer": {
|
||||
"liveChatPaidMessageRenderer": {
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
}
|
||||
{
|
||||
"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": {
|
||||
"liveChatPaidMessageRenderer": {
|
||||
"id": "dummy_id",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"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
|
||||
},
|
||||
"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,
|
||||
"contextMenuAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "コメントの操作"
|
||||
}
|
||||
"liveChatItemContextMenuEndpoint": {
|
||||
"params": "___params___"
|
||||
}
|
||||
},
|
||||
"timestampColor": 2147483648,
|
||||
"contextMenuAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "コメントの操作"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
|
||||
"fullDurationSec": 120
|
||||
}
|
||||
},
|
||||
"durationSec": "120"
|
||||
}
|
||||
}
|
||||
},
|
||||
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
|
||||
"fullDurationSec": 120
|
||||
}
|
||||
},
|
||||
"durationSec": "120"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
174
tests/testdata/default/supersticker.json
vendored
174
tests/testdata/default/supersticker.json
vendored
@@ -1,99 +1,97 @@
|
||||
{
|
||||
"response": {
|
||||
"responseContext": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
"responseContext": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatPaidStickerRenderer": {
|
||||
"id": "dummy_id",
|
||||
"contextMenuEndpoint": {
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"ignoreNavigation": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"addChatItemAction": {
|
||||
"item": {
|
||||
"liveChatPaidStickerRenderer": {
|
||||
"id": "dummy_id",
|
||||
"contextMenuEndpoint": {
|
||||
"commandMetadata": {
|
||||
"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": {
|
||||
"label": "コメントの操作"
|
||||
"label": "___sticker_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
|
||||
},
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
"responseContext": {
|
||||
"webResponseContextExtensionData": ""
|
||||
},
|
||||
"continuationContents": {
|
||||
"liveChatContinuation": {
|
||||
"continuations": [
|
||||
{
|
||||
"invalidationContinuationData": {
|
||||
"invalidationId": {
|
||||
"objectSource": 1000,
|
||||
"objectId": "___objectId___",
|
||||
"topic": "chat~00000000000~0000000",
|
||||
"subscribeToGcmTopics": true,
|
||||
"protoCreationTimestampMs": "1577804400000"
|
||||
},
|
||||
"timeoutMs": 10000,
|
||||
"continuation": "___continuation___"
|
||||
}
|
||||
],
|
||||
"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/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
|
||||
"width": 64,
|
||||
"height": 64
|
||||
}
|
||||
]
|
||||
},
|
||||
"contextMenuEndpoint": {
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"ignoreNavigation": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"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
|
||||
},
|
||||
"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",
|
||||
"timestampUsec": 1570678496000000,
|
||||
"authorExternalChannelId": "author_channel_id",
|
||||
"contextMenuAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "コメントの操作"
|
||||
}
|
||||
"liveChatItemContextMenuEndpoint": {
|
||||
"params": "___params___"
|
||||
}
|
||||
},
|
||||
"id": "dummy_id",
|
||||
"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
6128
tests/testdata/extract_duplcheck/head/dp0-1.json
vendored
6128
tests/testdata/extract_duplcheck/head/dp0-1.json
vendored
File diff suppressed because it is too large
Load Diff
6128
tests/testdata/extract_duplcheck/head/dp0-2.json
vendored
6128
tests/testdata/extract_duplcheck/head/dp0-2.json
vendored
File diff suppressed because it is too large
Load Diff
6128
tests/testdata/extract_duplcheck/head/dp0-3.json
vendored
6128
tests/testdata/extract_duplcheck/head/dp0-3.json
vendored
File diff suppressed because it is too large
Load Diff
5030
tests/testdata/extract_duplcheck/head/dp0-4.json
vendored
5030
tests/testdata/extract_duplcheck/head/dp0-4.json
vendored
File diff suppressed because it is too large
Load Diff
2834
tests/testdata/extract_duplcheck/head/dp0-5.json
vendored
2834
tests/testdata/extract_duplcheck/head/dp0-5.json
vendored
File diff suppressed because it is too large
Load Diff
12054
tests/testdata/extract_duplcheck/overlap/dp0-0.json
vendored
12054
tests/testdata/extract_duplcheck/overlap/dp0-0.json
vendored
File diff suppressed because it is too large
Load Diff
6128
tests/testdata/extract_duplcheck/overlap/dp0-1.json
vendored
6128
tests/testdata/extract_duplcheck/overlap/dp0-1.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