File Coverage

blib/lib/WWW/Twitch.pm
Criterion Covered Total %
statement 18 54 33.3
branch n/a
condition 0 6 0.0
subroutine 6 11 54.5
pod 4 5 80.0
total 28 76 36.8


line stmt bran cond sub pod time code
1             package WWW::Twitch;
2 1     1   1026 use Moo 2;
  1         9911  
  1         5  
3 1     1   1255 use feature 'signatures';
  1         2  
  1         77  
4 1     1   4 no warnings 'experimental::signatures';
  1         2  
  1         25  
5              
6 1     1   614 use HTTP::Tiny;
  1         43060  
  1         39  
7 1     1   638 use JSON 'encode_json', 'decode_json';
  1         7098  
  1         7  
8 1     1   148 use POSIX 'strftime';
  1         2  
  1         7  
9              
10             our $VERSION = '0.01';
11              
12             =head1 NAME
13              
14             WWW::Twitch - automate parts of Twitch without the need for an API key
15              
16             =head1 SYNOPSIS
17              
18             use 5.012; # say
19             use WWW::Twitch;
20              
21             my $channel = 'corion_de';
22             my $twitch = WWW::Twitch->new();
23             my $info = $twitch->live_stream($channel);
24             if( $info ) {
25             my $id = $info->{id};
26              
27             opendir my $dh, '.'
28             or die "$!";
29              
30             # If we have stale recordings, maybe our network went down
31             # in between
32             my @recordings = grep { /\b$id\.mp4(\.part)?$/ && -M $_ < 30/24/60/60 }
33             readdir $dh;
34              
35             if( ! @recordings ) {
36             say "$channel is live (Stream $id)";
37             say "Launching youtube-dl";
38             exec "youtube_dl", '-q', "https://www.twitch.tv/$channel";
39             } else {
40             say "$channel is recording (@recordings)";
41             };
42              
43             } else {
44             say "$channel is offline";
45             }
46              
47              
48             =cut
49              
50             =head1 METHODS
51              
52             =head2 C<< ->new >>
53              
54             my $twitch = WWW::Twitch->new();
55              
56             Creates a new Twitch client
57              
58             =over 4
59              
60             =item B
61              
62             Optional device id. If missing, a hardcoded
63             device id will be used.
64              
65             =item B
66              
67             Optional client id. If missing, a hardcoded
68             client id will be used.
69              
70             =item B
71              
72             Optional client version. If missing, a hardcoded
73             client version will be used.
74              
75             =item B
76              
77             Optional HTTP user agent. If missing, a L
78             object will be constructed.
79              
80             =back
81              
82             =cut
83              
84             has 'device_id' => (
85             is => 'ro',
86             default => 'WQS1BrvLDgmo6QcdpHY7M3d4eMRjf6ji'
87             );
88             has 'client_id' => (
89             is => 'ro',
90             default => 'kimne78kx3ncx6brgo4mv6wki5h1ko'
91             );
92             has 'client_version' => (
93             is => 'ro',
94             default => '2be2ebe0-0a30-4b77-b67e-de1ee11bcf9b',
95             );
96             has 'ua' =>
97             is => 'lazy',
98             default => sub {
99             HTTP::Tiny->new( verify_SSL => 1 ),
100             };
101              
102 0     0 0   sub fetch_gql( $self, $query ) {
  0            
  0            
  0            
103 0           my $res = $self->ua->post( 'https://gql.twitch.tv/gql', {
104             content => encode_json( $query ),
105             headers => {
106             # so far we need no headers
107             "Client-ID" => $self->client_id,
108             },
109             });
110 0           $res = decode_json( $res->{content} );
111             }
112              
113             =head2 C<< ->schedule( $channel ) >>
114              
115             my $schedule = $twitch->schedule( 'somechannel', %options );
116              
117             Fetch the schedule of a channel
118              
119             =cut
120              
121 0     0 1   sub schedule( $self, $channel, %options ) {
  0            
  0            
  0            
  0            
122 0   0       $options{ start_at } //= strftime '%Y-%m-%dT%H:%M:%SZ', gmtime(time);
123 0   0       $options{ end_at } //= strftime '%Y-%m-%dT%H:%M:%SZ', gmtime(time+24*7*3600);
124 0           warn $options{ start_at };
125 0           warn $options{ end_at };
126             my $res =
127             $self->fetch_gql( [{"operationName" => "StreamSchedule",
128             "variables" => { "login" => $channel,
129             "startingWeekday" => "MONDAY",
130             "utcOffsetMinutes" => 120,
131             "startAt" => $options{ start_at },
132             "endAt" => $options{ end_at }
133             },
134 0           "extensions" => {
135             "persistedQuery" => {
136             "version" => 1,
137             "sha256Hash" => "d495cb17a67b6f7a8842e10297e57dcd553ea17fe691db435e39a618fe4699cf"
138             }
139             }
140             }]
141             );
142             #use Data::Dumper;
143             #warn Dumper $res;
144 0           return $res->[0]->{data}->{user}->{channel}->{schedule};
145             };
146              
147             =head2 C<< ->is_live( $channel ) >>
148              
149             if( $twitch->is_live( 'somechannel' ) ) {
150             ...
151             }
152              
153             Check whether a stream is currently live on a channel
154              
155             =cut
156              
157 0     0 1   sub is_live( $self, $channel ) {
  0            
  0            
  0            
158 0           my $res =
159             $self->fetch_gql([{"operationName" => "WithIsStreamLiveQuery",
160             "variables" => {"id" => "50985620"},
161             "extensions" => {
162             "persistedQuery" => {
163             "version" => 1,
164             "sha256Hash" => "04e46329a6786ff3a81c01c50bfa5d725902507a0deb83b0edbf7abe7a3716ea"
165             }
166             }
167             },
168             #{"operationName" => "ChannelPollContext_GetViewablePoll",
169             # "variables" => {"login" => "papaplatte"},
170             # "extensions" => {"persistedQuery" => {"version" => 1,"sha256Hash" => "d37a38ac165e9a15c26cd631d70070ee4339d48ff4975053e622b918ce638e0f"}}}
171             ]
172             #"Client-Version": "9ea2055a-41f0-43b7-b295-70885b40c41c",
173             );
174 0           return $res->[0]->{data};
175             }
176              
177             =head2 C<< ->stream_playback_access_token( $channel ) >>
178              
179             my $tok = $twitch->stream_playback_access_token( 'somechannel' );
180             say $tok->{channel_id};
181              
182             Internal method to fetch the stream playback access token
183              
184             =cut
185              
186 0     0 1   sub stream_playback_access_token( $self, $channel ) {
  0            
  0            
  0            
187 0           my $res =
188             $self->fetch_gql([{"operationName" => "PlaybackAccessToken_Template",
189             "query" => 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "web", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "web", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}',
190             "variables" => {"isLive" => $JSON::true,"login" => "$channel","isVod" => $JSON::false,"vodID" => "","playerType" => "site"}},
191             ]);
192 0           return decode_json( $res->[0]->{data}->{streamPlaybackAccessToken}->{value} );
193             };
194              
195             =head2 C<< ->live_stream( $channel ) >>
196              
197             my $tok = $twitch->live_stream( 'somechannel' );
198              
199             Internal method to fetch information about a stream on a channel
200              
201             =cut
202              
203 0     0 1   sub live_stream( $self, $channel ) {
  0            
  0            
  0            
204 0           my $id = $self->stream_playback_access_token( $channel )->{channel_id};
205 0           my $res =
206             $self->fetch_gql(
207             [{"operationName" => "WithIsStreamLiveQuery","variables" => {"id" => "$id"},
208             "extensions" => {"persistedQuery" => {"version" => 1,"sha256Hash" => "04e46329a6786ff3a81c01c50bfa5d725902507a0deb83b0edbf7abe7a3716ea"}}},
209             ]);
210              
211 0           return $res->[0]->{data}->{user}->{stream};
212             }
213              
214             #curl 'https://gql.twitch.tv/gql#origin=twilight'
215             # -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0'
216             # -H 'Accept: */*'
217             # -H 'Accept-Language: de-DE'
218             # --compressed
219             # -H 'Referer: https://www.twitch.tv/'
220             # -H 'Client-Id: kimne78kx3ncx6brgo4mv6wki5h1ko'
221             # -H 'X-Device-Id: WQS1BrvLDgmo6QcdpHY7M3d4eMRjf6ji'
222             # -H 'Client-Version: 2be2ebe0-0a30-4b77-b67e-de1ee11bcf9b'
223             # -H 'Content-Type: text/plain;charset=UTF-8'
224             # -H 'Origin: https://www.twitch.tv'
225             # -H 'DNT: 1'
226             # -H 'Connection: keep-alive'
227             # -H 'Sec-Fetch-Dest: empty'
228             # -H 'Sec-Fetch-Mode: cors'
229             # -H 'Sec-Fetch-Site: same-site'
230             # --data-raw '[{"operationName":"StreamSchedule","variables":{"login":"bootiemashup","startingWeekday":"MONDAY","utcOffsetMinutes":120,"startAt":"2021-07-25T22:00:00.000Z","endAt":"2021-08-01T21:59:59.059Z"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"e9af1b7aa4c4eaa1655a3792147c4dd21aacd561f608e0933c3c5684d9b607a6"}}}]'
231              
232             1;