Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
128a834841 | ||
|
|
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 |
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]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt -r requirements_test.txt
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
export PYTHONPATH=./
|
||||||
|
pytest --verbose --color=yes
|
||||||
39
README.md
39
README.md
@@ -24,12 +24,14 @@ pip install pytchat
|
|||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
One-liner command.
|
+ One-liner command.
|
||||||
|
|
||||||
|
+ Save chat data to html with embedded custom emojis.
|
||||||
|
|
||||||
|
+ Show chat stream (--echo option).
|
||||||
|
|
||||||
Save chat data to html with embedded custom emojis.
|
|
||||||
Show chat stream (--echo option).
|
|
||||||
```bash
|
```bash
|
||||||
$ pytchat -v https://www.youtube.com/watch?v=uIx8l2xlYVY -o "c:/temp/"
|
$ pytchat -v uIx8l2xlYVY -o "c:/temp/"
|
||||||
# options:
|
# options:
|
||||||
# -v : Video ID or URL that includes ID
|
# -v : Video ID or URL that includes ID
|
||||||
# -o : output directory (default path: './')
|
# -o : output directory (default path: './')
|
||||||
@@ -38,7 +40,7 @@ $ pytchat -v https://www.youtube.com/watch?v=uIx8l2xlYVY -o "c:/temp/"
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### On-demand mode with simple non-buffered object.
|
### Fetch chat data (see [wiki](https://github.com/taizan-hokuto/pytchat/wiki/PytchatCore))
|
||||||
```python
|
```python
|
||||||
import pytchat
|
import pytchat
|
||||||
chat = pytchat.create(video_id="uIx8l2xlYVY")
|
chat = pytchat.create(video_id="uIx8l2xlYVY")
|
||||||
@@ -47,7 +49,8 @@ while chat.is_alive():
|
|||||||
print(f"{c.datetime} [{c.author.name}]- {c.message}")
|
print(f"{c.datetime} [{c.author.name}]- {c.message}")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Output JSON format (feature of [DefaultProcessor](DefaultProcessor))
|
|
||||||
|
### Output JSON format string (feature of [DefaultProcessor](https://github.com/taizan-hokuto/pytchat/wiki/DefaultProcessor))
|
||||||
```python
|
```python
|
||||||
import pytchat
|
import pytchat
|
||||||
import time
|
import time
|
||||||
@@ -58,35 +61,21 @@ while chat.is_alive():
|
|||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
'''
|
'''
|
||||||
# Each chat item can also be output in JSON format.
|
# Each chat item can also be output in JSON format.
|
||||||
for c in chat.get().sync_items():
|
for c in chat.get().items:
|
||||||
print(c.json())
|
print(c.json())
|
||||||
'''
|
'''
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### other
|
### other
|
||||||
#### Fetch chat with buffer.
|
+ Fetch chat with a buffer ([LiveChat](https://github.com/taizan-hokuto/pytchat/wiki/LiveChat))
|
||||||
[LiveChat](https://github.com/taizan-hokuto/pytchat/wiki/LiveChat)
|
|
||||||
|
|
||||||
#### Asyncio Context
|
+ Use with asyncio ([LiveChatAsync](https://github.com/taizan-hokuto/pytchat/wiki/LiveChatAsync))
|
||||||
[LiveChatAsync](https://github.com/taizan-hokuto/pytchat/wiki/LiveChatAsync)
|
|
||||||
|
|
||||||
#### [YT API compatible chat processor]https://github.com/taizan-hokuto/pytchat/wiki/CompatibleProcessor)
|
+ YT API compatible chat processor ([CompatibleProcessor](https://github.com/taizan-hokuto/pytchat/wiki/CompatibleProcessor))
|
||||||
|
|
||||||
### [Extract archived chat data](https://github.com/taizan-hokuto/pytchat/wiki/Extractor)
|
+ Extract archived chat data ([Extractor](https://github.com/taizan-hokuto/pytchat/wiki/Extractor))
|
||||||
```python
|
|
||||||
from pytchat import HTMLArchiver, Extractor
|
|
||||||
|
|
||||||
video_id = "*******"
|
|
||||||
ex = Extractor(
|
|
||||||
video_id,
|
|
||||||
div=10,
|
|
||||||
processor=HTMLArchiver("c:/test.html")
|
|
||||||
)
|
|
||||||
|
|
||||||
ex.extract()
|
|
||||||
print("finished.")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Structure of Default Processor
|
## Structure of Default Processor
|
||||||
Each item can be got with `sync_items()` function.
|
Each item can be got with `sync_items()` function.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
pytchat is a lightweight python library to browse youtube livechat without Selenium or BeautifulSoup.
|
pytchat is a lightweight python library to browse youtube livechat without Selenium or BeautifulSoup.
|
||||||
"""
|
"""
|
||||||
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
|
__copyright__ = 'Copyright (C) 2019, 2020 taizan-hokuto'
|
||||||
__version__ = '0.4.0'
|
__version__ = '0.4.4'
|
||||||
__license__ = 'MIT'
|
__license__ = 'MIT'
|
||||||
__author__ = 'taizan-hokuto'
|
__author__ = 'taizan-hokuto'
|
||||||
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class HTMLArchiver(ChatProcessor):
|
|||||||
self.client = httpx.Client(http2=True)
|
self.client = httpx.Client(http2=True)
|
||||||
self.save_path = self._checkpath(save_path)
|
self.save_path = self._checkpath(save_path)
|
||||||
self.processor = DefaultProcessor()
|
self.processor = DefaultProcessor()
|
||||||
self.emoji_table = {} # tuble for custom emojis. key: emoji_id, value: base64 encoded image binary.
|
self.emoji_table = {} # dict for custom emojis. key: emoji_id, value: base64 encoded image binary.
|
||||||
self.header = [HEADER_HTML]
|
self.header = [HEADER_HTML]
|
||||||
self.body = ['<body>\n', '<table class="css">\n', self._parse_table_header(fmt_headers)]
|
self.body = ['<body>\n', '<table class="css">\n', self._parse_table_header(fmt_headers)]
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
@@ -123,7 +123,6 @@ class HTMLArchiver(ChatProcessor):
|
|||||||
resp = self.client.get(url, timeout=30)
|
resp = self.client.get(url, timeout=30)
|
||||||
break
|
break
|
||||||
except httpx.HTTPError as e:
|
except httpx.HTTPError as e:
|
||||||
print("Network Error. retrying...")
|
|
||||||
err = e
|
err = e
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
else:
|
else:
|
||||||
@@ -132,7 +131,7 @@ class HTMLArchiver(ChatProcessor):
|
|||||||
return standard_b64encode(resp.content).decode()
|
return standard_b64encode(resp.content).decode()
|
||||||
|
|
||||||
def _set_emoji_table(self, item: dict):
|
def _set_emoji_table(self, item: dict):
|
||||||
emoji_id = item['id']
|
emoji_id = ''.join(('Z', item['id'])) if 48 <= ord(item['id'][0]) <= 57 else item['id']
|
||||||
if emoji_id not in self.emoji_table:
|
if emoji_id not in self.emoji_table:
|
||||||
self.emoji_table.setdefault(emoji_id, self.executor.submit(self._encode_img, item['url']))
|
self.emoji_table.setdefault(emoji_id, self.executor.submit(self._encode_img, item['url']))
|
||||||
return emoji_id
|
return emoji_id
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
httpx[http2]==0.14.1
|
httpx[http2]==0.16.1
|
||||||
protobuf==3.13.0
|
protobuf==3.14.0
|
||||||
pytz
|
pytz
|
||||||
urllib3
|
urllib3
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
mock
|
pytest-mock==3.3.1
|
||||||
mocker
|
pytest-httpx==0.10.0
|
||||||
pytest
|
|
||||||
pytest_httpx
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from pytchat.parser.live import Parser
|
from pytchat.parser.live import Parser
|
||||||
from pytchat.processors.default.processor import DefaultProcessor
|
from pytchat.processors.default.processor import DefaultProcessor
|
||||||
|
|
||||||
|
|
||||||
|
TEST_TIMETSTAMP = 1570678496000000
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_datetime(timestamp):
|
||||||
|
dt = datetime.fromtimestamp(timestamp / 1000000)
|
||||||
|
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
|
||||||
def test_textmessage(mocker):
|
def test_textmessage(mocker):
|
||||||
'''text message'''
|
'''text message'''
|
||||||
processor = DefaultProcessor()
|
processor = DefaultProcessor()
|
||||||
@@ -20,7 +29,7 @@ def test_textmessage(mocker):
|
|||||||
assert ret.id == "dummy_id"
|
assert ret.id == "dummy_id"
|
||||||
assert ret.message == "dummy_message"
|
assert ret.message == "dummy_message"
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.author.name == "author_name"
|
assert ret.author.name == "author_name"
|
||||||
assert ret.author.channelId == "author_channel_id"
|
assert ret.author.channelId == "author_channel_id"
|
||||||
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
|
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
|
||||||
@@ -51,7 +60,7 @@ def test_textmessage_replay_member(mocker):
|
|||||||
assert ret.message == "dummy_message"
|
assert ret.message == "dummy_message"
|
||||||
assert ret.messageEx == ["dummy_message"]
|
assert ret.messageEx == ["dummy_message"]
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == "1:23:45"
|
assert ret.elapsedTime == "1:23:45"
|
||||||
assert ret.author.name == "author_name"
|
assert ret.author.name == "author_name"
|
||||||
assert ret.author.channelId == "author_channel_id"
|
assert ret.author.channelId == "author_channel_id"
|
||||||
@@ -83,7 +92,7 @@ def test_superchat(mocker):
|
|||||||
assert ret.message == "dummy_message"
|
assert ret.message == "dummy_message"
|
||||||
assert ret.messageEx == ["dummy_message"]
|
assert ret.messageEx == ["dummy_message"]
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == ""
|
assert ret.elapsedTime == ""
|
||||||
assert ret.amountValue == 800
|
assert ret.amountValue == 800
|
||||||
assert ret.amountString == "¥800"
|
assert ret.amountString == "¥800"
|
||||||
@@ -125,7 +134,7 @@ def test_supersticker(mocker):
|
|||||||
assert ret.message == ""
|
assert ret.message == ""
|
||||||
assert ret.messageEx == []
|
assert ret.messageEx == []
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == ""
|
assert ret.elapsedTime == ""
|
||||||
assert ret.amountValue == 200
|
assert ret.amountValue == 200
|
||||||
assert ret.amountString == "¥200"
|
assert ret.amountString == "¥200"
|
||||||
@@ -166,7 +175,7 @@ def test_sponsor(mocker):
|
|||||||
assert ret.message == "新規メンバー"
|
assert ret.message == "新規メンバー"
|
||||||
assert ret.messageEx == ["新規メンバー"]
|
assert ret.messageEx == ["新規メンバー"]
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == ""
|
assert ret.elapsedTime == ""
|
||||||
assert ret.bgColor == 0
|
assert ret.bgColor == 0
|
||||||
assert ret.author.name == "author_name"
|
assert ret.author.name == "author_name"
|
||||||
@@ -199,7 +208,7 @@ def test_sponsor_legacy(mocker):
|
|||||||
assert ret.message == "新規メンバー / ようこそ、author_name!"
|
assert ret.message == "新規メンバー / ようこそ、author_name!"
|
||||||
assert ret.messageEx == ["新規メンバー / ようこそ、author_name!"]
|
assert ret.messageEx == ["新規メンバー / ようこそ、author_name!"]
|
||||||
assert ret.timestamp == 1570678496000
|
assert ret.timestamp == 1570678496000
|
||||||
assert ret.datetime == "2019-10-10 12:34:56"
|
assert ret.datetime == get_local_datetime(TEST_TIMETSTAMP)
|
||||||
assert ret.elapsedTime == ""
|
assert ret.elapsedTime == ""
|
||||||
assert ret.bgColor == 0
|
assert ret.bgColor == 0
|
||||||
assert ret.author.name == "author_name"
|
assert ret.author.name == "author_name"
|
||||||
|
|||||||
Reference in New Issue
Block a user