Skip to content

Instantly share code, notes, and snippets.

@tung
Last active June 11, 2025 20:10
Show Gist options
  • Save tung/20de3e992ca3a6629843e8169dc0398e to your computer and use it in GitHub Desktop.
Save tung/20de3e992ca3a6629843e8169dc0398e to your computer and use it in GitHub Desktop.

Revisions

  1. tung revised this gist Mar 10, 2023. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions twitch-vod-chat.py
    Original file line number Diff line number Diff line change
    @@ -79,6 +79,8 @@ def badge_icons(message):


    def simple_name(commenter):
    if commenter == None:
    return "(null)"
    name = commenter['login']
    display_name = commenter['displayName']
    if display_name:
  2. tung revised this gist Dec 16, 2022. 1 changed file with 41 additions and 33 deletions.
    74 changes: 41 additions & 33 deletions twitch-vod-chat.py
    Original file line number Diff line number Diff line change
    @@ -49,8 +49,8 @@ def message_color(comment):
    r = 128
    g = 128
    b = 128
    if 'user_color' in comment['message']:
    user_color = comment['message']['user_color']
    if 'userColor' in comment['message'] and comment['message']['userColor']:
    user_color = comment['message']['userColor']
    if len(user_color) == 7 and user_color[0] == '#':
    r = int(user_color[1:3], 16)
    g = int(user_color[3:5], 16)
    @@ -65,22 +65,22 @@ def message_color(comment):

    def badge_icons(message):
    b = ''
    if 'user_badges' in message:
    for badge in message['user_badges']:
    if badge['_id'] == 'broadcaster':
    if 'userBadges' in message:
    for badge in message['userBadges']:
    if badge['setID'] == 'broadcaster':
    b += '🎥'
    elif badge['_id'] == 'moderator':
    elif badge['setID'] == 'moderator':
    b += '⚔'
    elif badge['_id'] == 'subscriber':
    elif badge['setID'] == 'subscriber':
    b += '★'
    elif badge['_id'] == 'staff':
    elif badge['setID'] == 'staff':
    b += '⛨'
    return b


    def simple_name(commenter):
    name = commenter['name']
    display_name = commenter['display_name']
    name = commenter['login']
    display_name = commenter['displayName']
    if display_name:
    c = display_name[0].lower()
    if (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_':
    @@ -89,20 +89,20 @@ def simple_name(commenter):


    def format_message(comment):
    time = comment['content_offset_seconds']
    time = comment['contentOffsetSeconds']
    badges = badge_icons(comment['message'])
    name = simple_name(comment['commenter'])
    color = lighten_color(message_color(comment))

    message = comment['message']['body']
    if comment['message']['is_action']:
    message = '\033[38;2;' + str(color.r) + ';' + str(color.g) + ';' + str(color.b) + 'm' + message + '\033[0m'
    message = "".join([f['text'] for f in comment['message']['fragments']])
    #if comment['message']['is_action']:
    # message = '\033[38;2;' + str(color.r) + ';' + str(color.g) + ';' + str(color.b) + 'm' + message + '\033[0m'

    nick = '\033[38;2;{c.r};{c.g};{c.b}m<{badges}{name}>\033[0m'.format(badges=badges, name=name, c=color)
    if 'user_badges' in comment['message']:
    if 'userBadges' in comment['message']:
    is_broadcaster = False
    for badge in comment['message']['user_badges']:
    if badge['_id'] == 'broadcaster':
    for badge in comment['message']['userBadges']:
    if badge['setID'] == 'broadcaster':
    is_broadcaster = True
    break
    if is_broadcaster:
    @@ -112,12 +112,10 @@ def format_message(comment):


    def print_response_messages(data, start):
    for comment in data['comments']:
    if comment['content_offset_seconds'] < start:
    for comment in data['comments']['edges']:
    if comment['node']['contentOffsetSeconds'] < start:
    continue
    if comment['source'] != 'chat':
    continue
    print(format_message(comment))
    print(format_message(comment['node']))


    ################################################################################
    @@ -129,28 +127,38 @@ def print_response_messages(data, start):
    start = int(sys.argv[2])

    session = requests.Session()
    session.headers = { 'Client-ID': os.environ['TWITCH_CLIENT_ID'], 'Accept': 'application/vnd.twitchtv.v5+json' }

    response = session.get('https://api.twitch.tv/v5/videos/' + video_id + '/comments?content_offset_seconds=' + str(start), timeout=10)
    session.headers = { 'Client-ID': os.environ['TWITCH_CLIENT_ID'], 'content-type': 'application/json' }

    response = session.post(
    'https://gql.twitch.tv/gql',
    "[{\"operationName\":\"VideoCommentsByOffsetOrCursor\"," +
    "\"variables\":{\"videoID\":\"" + video_id + "\",\"contentOffsetSeconds\":" + str(start) + "}," +
    "\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"b70a3591ff0f4e0313d126c6a1502d79a1c02baebb288227c582044aa76adf6a\"}}}]",
    timeout=10)
    response.raise_for_status()
    data = response.json()

    print_response_messages(data, start)
    print_response_messages(data[0]['data']['video'], start)

    cursor = None
    if '_next' in data:
    cursor = data['_next']
    if data[0]['data']['video']['comments']['pageInfo']['hasNextPage']:
    cursor = data[0]['data']['video']['comments']['edges'][-1]['cursor']
    time.sleep(0.1)

    while cursor:
    response = session.get('https://api.twitch.tv/v5/videos/' + video_id + '/comments?cursor=' + cursor, timeout=10)
    response = session.post(
    'https://gql.twitch.tv/gql',
    "[{\"operationName\":\"VideoCommentsByOffsetOrCursor\"," +
    "\"variables\":{\"videoID\":\"" + video_id + "\",\"cursor\":\"" + cursor + "\"}," +
    "\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"b70a3591ff0f4e0313d126c6a1502d79a1c02baebb288227c582044aa76adf6a\"}}}]",
    timeout=10)
    response.raise_for_status()
    data = response.json()

    print_response_messages(data, start)
    print_response_messages(data[0]['data']['video'], start)

    if '_next' in data:
    cursor = data['_next']
    if data[0]['data']['video']['comments']['pageInfo']['hasNextPage']:
    cursor = data[0]['data']['video']['comments']['edges'][-1]['cursor']
    time.sleep(0.1)
    else:
    cursor = None
    cursor = None
  3. tung revised this gist Oct 12, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion twitch-vod-chat.py
    Original file line number Diff line number Diff line change
    @@ -96,7 +96,7 @@ def format_message(comment):

    message = comment['message']['body']
    if comment['message']['is_action']:
    message = '\033[38;2;' + color.r + ';' + color.g + ';' + color.b + 'm' + message + '\033[0m'
    message = '\033[38;2;' + str(color.r) + ';' + str(color.g) + ';' + str(color.b) + 'm' + message + '\033[0m'

    nick = '\033[38;2;{c.r};{c.g};{c.b}m<{badges}{name}>\033[0m'.format(badges=badges, name=name, c=color)
    if 'user_badges' in comment['message']:
  4. tung created this gist Oct 11, 2017.
    156 changes: 156 additions & 0 deletions twitch-vod-chat.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,156 @@
    #!/usr/bin/env python3

    #
    # A script to download chat from a Twitch VOD and print it to a terminal.
    # Chat will be downloaded all the way until it ends.
    #
    # Usage: TWITCH_CLIENT_ID=0123456789abcdef0123456789abcde twitch-vod-chat.py [video_id] [start]
    #
    # This script could break at any time, because Twitch's chat API is
    # undocumented and likes to change at any time; in fact, this script was
    # created because Twitch got rid of rechat.twitch.tv.
    #

    import json
    import os
    import requests
    import sys
    import time
    import types


    def time_to_hhmmss(t):
    hours = int(t // 3600)
    minutes = int((t - hours * 3600) // 60)
    seconds = int(t - hours * 3600 - minutes * 60)
    milliseconds = int((t - hours * 3600 - minutes * 60 - seconds) * 1000)
    return "{0}:{1:02}:{2:02}.{3:<03}".format(hours, minutes, seconds, milliseconds)


    def lighten_color(color):
    r = color.r * 3 // 4 + 63
    g = color.g * 3 // 4 + 63
    b = color.b * 3 // 4 + 63
    return types.SimpleNamespace(r=r, g=g, b=b)


    def message_color(comment):
    color_by_name = {
    'white': (255, 255, 255),
    'black': (0, 0, 0),
    'red': (255, 0, 0),
    'green': (0, 255, 0),
    'blue': (0, 0, 255),
    'yellow': (255, 255, 0),
    'gray': (128, 128, 128),
    'magenta': (255, 0, 255),
    'cyan': (0, 255, 255)
    }
    r = 128
    g = 128
    b = 128
    if 'user_color' in comment['message']:
    user_color = comment['message']['user_color']
    if len(user_color) == 7 and user_color[0] == '#':
    r = int(user_color[1:3], 16)
    g = int(user_color[3:5], 16)
    b = int(user_color[5:7], 16)
    elif user_color in color_by_name:
    c = color_by_name[user_color]
    r = c[0]
    g = c[1]
    b = c[2]
    return types.SimpleNamespace(r=r, g=g, b=b)


    def badge_icons(message):
    b = ''
    if 'user_badges' in message:
    for badge in message['user_badges']:
    if badge['_id'] == 'broadcaster':
    b += '🎥'
    elif badge['_id'] == 'moderator':
    b += '⚔'
    elif badge['_id'] == 'subscriber':
    b += '★'
    elif badge['_id'] == 'staff':
    b += '⛨'
    return b


    def simple_name(commenter):
    name = commenter['name']
    display_name = commenter['display_name']
    if display_name:
    c = display_name[0].lower()
    if (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_':
    name = display_name
    return name


    def format_message(comment):
    time = comment['content_offset_seconds']
    badges = badge_icons(comment['message'])
    name = simple_name(comment['commenter'])
    color = lighten_color(message_color(comment))

    message = comment['message']['body']
    if comment['message']['is_action']:
    message = '\033[38;2;' + color.r + ';' + color.g + ';' + color.b + 'm' + message + '\033[0m'

    nick = '\033[38;2;{c.r};{c.g};{c.b}m<{badges}{name}>\033[0m'.format(badges=badges, name=name, c=color)
    if 'user_badges' in comment['message']:
    is_broadcaster = False
    for badge in comment['message']['user_badges']:
    if badge['_id'] == 'broadcaster':
    is_broadcaster = True
    break
    if is_broadcaster:
    nick = '\033[7m' + nick

    return "\033[94m{time}\033[0m {nick} {message}".format(time=time_to_hhmmss(time), nick=nick, message=message)


    def print_response_messages(data, start):
    for comment in data['comments']:
    if comment['content_offset_seconds'] < start:
    continue
    if comment['source'] != 'chat':
    continue
    print(format_message(comment))


    ################################################################################

    if len(sys.argv) < 3 or 'TWITCH_CLIENT_ID' not in os.environ:
    print('Usage: TWITCH_CLIENT_ID=[client_id] {0} [video_id] [start]'.format(sys.argv[0]), file=sys.stderr)
    sys.exit(1)
    video_id = sys.argv[1]
    start = int(sys.argv[2])

    session = requests.Session()
    session.headers = { 'Client-ID': os.environ['TWITCH_CLIENT_ID'], 'Accept': 'application/vnd.twitchtv.v5+json' }

    response = session.get('https://api.twitch.tv/v5/videos/' + video_id + '/comments?content_offset_seconds=' + str(start), timeout=10)
    response.raise_for_status()
    data = response.json()

    print_response_messages(data, start)

    cursor = None
    if '_next' in data:
    cursor = data['_next']
    time.sleep(0.1)

    while cursor:
    response = session.get('https://api.twitch.tv/v5/videos/' + video_id + '/comments?cursor=' + cursor, timeout=10)
    response.raise_for_status()
    data = response.json()

    print_response_messages(data, start)

    if '_next' in data:
    cursor = data['_next']
    time.sleep(0.1)
    else:
    cursor = None