├── LICENSE ├── README.md ├── demo.png ├── demolive.png ├── demolivefinished.png ├── ytnotify.php ├── ytnotify_subscribe.php └── ytnotify_subscribe.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## YouTube upload/livestream notification via Discord webhook 2 | 3 | **Notifies for:** 4 | 5 | - Standard video upload: 6 | 7 | ![Standard video upload](demo.png) 8 | 9 | 10 | - Livestreams starting (option for gaming.youtube.com link, enabled by default): 11 | 12 | ![Livestream starting](demolive.png) 13 | 14 | 15 | - Livestreams that have just ended, with a link to watch (option to disable): 16 | 17 | ![Livestream finished](demolivefinished.png) 18 | 19 | 20 | Usually is faster than an email from YouTube of the same video 21 | 22 | 23 | 24 | **Requirements:** 25 | - PHP webserver with curl 26 | * (could easily be converted to some other server/language though) 27 | - Permission for PHP to create a file in the same directory 28 | * I assumed this was pretty standard, but I ran into it recently so I'm listing it here. After a notification shows in Discord, there should be a `ytnotify.latest` file on the webserver - if not, something is broken. 29 | 30 | 31 | **Setup:** 32 | - Create a webhook on Discord (edit a text channel > Webhooks > Create Webhook) 33 | - Follow steps 1 - 3 under "Before you start" on https://developers.google.com/youtube/v3/getting-started to create a server API key 34 | - Edit ytnotify.php with a text editor: 35 | * Change REPLACE_WITH_API_KEY to your server API key created above 36 | * Change REPLACE_WITH_CHANNEL_ID to your YouTube channel ID (more info: https://developers.google.com/youtube/v3/guides/working_with_channel_ids) 37 | * Change REPLACE_WITH_UNIQUE_SECRET to your own unique secret - If you aren't sure what to put, grab something from here: https://www.randomlists.com/string 38 | * Change REPLACE_WITH_WEBHOOK_URL to your Discord webhook URL 39 | - Upload ytnotify.php to a public location on your webserver 40 | - Edit ytnotify_subscribe.sh/php with a text editor: 41 | * Change REPLACE_WITH_CHANNEL_ID to your YouTube channel ID 42 | * Change REPLACE_WITH_CALLBACK_URL to the public URL of ytnotify.php (including http[s]://) 43 | * Change REPLACE_WITH_UNIQUE_SECRET to the same secret set in ytnotify.php 44 | 45 | ytnotify_subscribe needs to be run regularly - the subscription times out after a set time (432000 seconds/5 days last I checked). 46 | This is best done with a cronjob on the server - I have mine run at 5am every Monday and Friday. 47 | 48 | Since it falls back to notifying when there's no last known publish date, the first notification could be from a title or description change. 49 | 50 | 51 | **Known issues:** 52 | - Keeping track of the last publish time with a file is probably not the best, and should be changed eventually... 53 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WinSuk/discord-youtube-notifier/992ac1442ef9c6dd3bef6dd7a20c2d4319621330/demo.png -------------------------------------------------------------------------------- /demolive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WinSuk/discord-youtube-notifier/992ac1442ef9c6dd3bef6dd7a20c2d4319621330/demolive.png -------------------------------------------------------------------------------- /demolivefinished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WinSuk/discord-youtube-notifier/992ac1442ef9c6dd3bef6dd7a20c2d4319621330/demolivefinished.png -------------------------------------------------------------------------------- /ytnotify.php: -------------------------------------------------------------------------------- 1 | entry->children("http://www.youtube.com/xml/schemas/2015")->videoId; 69 | 70 | // First, determine if this is a livestream or not, and the status of the livestream 71 | $url = "https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=$id&maxResults=1&key=" . APIKEY; 72 | $json = json_decode(file_get_contents($url), true); 73 | $item = $json['items'][0]; 74 | $isFinishedLiveStream = false; 75 | $isInProgressLiveStream = false; 76 | if (array_key_exists('liveStreamingDetails', $item)) { 77 | $stream = $item['liveStreamingDetails']; 78 | if ($stream['actualStartTime'] != null) { 79 | // This is/was a livestream 80 | if ($stream['actualEndTime'] != null) { 81 | // This was a livestream that is now finished 82 | $isFinishedLiveStream = true; 83 | } else { 84 | // This is a livestream that is currently LIVE 85 | $isInProgressLiveStream = true; 86 | } 87 | } else { 88 | // This is an upcoming livestream that hasn't gone live yet. 89 | // It can be very dangerous: when a stream ends, a new ID will be 90 | // generated for the next stream, with the publish time set to NOW. 91 | // Discard completely. 92 | die(); 93 | } 94 | } 95 | $isLiveStream = ($isFinishedLiveStream || $isInProgressLiveStream); 96 | 97 | 98 | $inputdate = ""; 99 | if ($isInProgressLiveStream) { 100 | $inputdate = $stream['actualStartTime']; 101 | } else if ($isFinishedLiveStream) { 102 | if (NOTIFY_COMPLETED_LIVESTREAMS) { 103 | $inputdate = $stream['actualEndTime']; 104 | } 105 | } else { 106 | $inputdate = $xml->entry->published; 107 | } 108 | 109 | $notify = false; 110 | if ($inputdate != "") { 111 | $latest = file_get_contents($LATEST_FILE); 112 | if ($latest == "") { 113 | // No last known video, so send the notification and hope for the best D: 114 | $notify = true; 115 | } else { 116 | // Test dates 117 | $pubdate = date_create($inputdate); 118 | $latestdate = date_create($latest); 119 | if ($pubdate > $latestdate) { 120 | // It's newer, notify! 121 | $notify = true; 122 | } 123 | } 124 | } 125 | 126 | if ($notify) { 127 | // Prepare the POST input 128 | $msg = ""; 129 | if ($isInProgressLiveStream) { 130 | $msg = "\xf0\x9f\x94\xb4 **Livestream started!** \xf0\x9f\x94\xb4"; 131 | } else if ($isFinishedLiveStream) { 132 | $msg = "A finished livestream is now available as a video:"; 133 | } else { 134 | $msg = "\xf0\x9f\x8e\x9e **NEW VIDEO!** \xf0\x9f\x8e\x9e"; 135 | } 136 | 137 | if ($isInProgressLiveStream && PREFER_GAMING_LINK) { 138 | $msg .= "\nhttps://gaming.youtube.com/watch?v=$id"; 139 | } else { 140 | $msg .= "\nhttps://www.youtube.com/watch?v=$id"; 141 | } 142 | 143 | $data = json_encode(array( 144 | 'content' => $msg 145 | )); 146 | 147 | // cURL away! 148 | $curl = curl_init(); 149 | curl_setopt_array($curl, array( 150 | CURLOPT_URL => WEBHOOKURL, 151 | CURLOPT_HTTPHEADER => array( 152 | 'Content-Type: application/json;charset=UTF-8' 153 | ), 154 | CURLOPT_POST => 1, 155 | CURLOPT_POSTFIELDS => $data, 156 | CURLOPT_RETURNTRANSFER => TRUE 157 | )); 158 | $response = curl_exec($curl); 159 | curl_close($curl); 160 | 161 | // Save latest date to file 162 | file_put_contents($LATEST_FILE, $inputdate); 163 | } 164 | 165 | ?> 166 | -------------------------------------------------------------------------------- /ytnotify_subscribe.php: -------------------------------------------------------------------------------- 1 | "https://pubsubhubbub.appspot.com/subscribe", 24 | CURLOPT_POST => 1, 25 | CURLOPT_POSTFIELDS => array( 26 | 'hub.mode' => 'subscribe', 27 | 'hub.topic' => 'https://www.youtube.com/xml/feeds/videos.xml?channel_id=' . $chid, 28 | 'hub.callback' => CALLBACKURL, 29 | 'hub.secret' => SECRET, 30 | 'hub.verify' => 'sync' 31 | ), 32 | CURLOPT_RETURNTRANSFER => TRUE 33 | )); 34 | $response = curl_exec($curl); 35 | curl_close($curl); 36 | 37 | echo "$response\n"; 38 | } 39 | 40 | echo "Done.\n"; 41 | 42 | ?> 43 | -------------------------------------------------------------------------------- /ytnotify_subscribe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### ~ Change these values! ~ ### 4 | 5 | # YouTube channel ID(s) 6 | # Can be multiple channels - eg: `CHANNELIDS=("aaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbb")` 7 | CHANNELIDS=("REPLACE_WITH_CHANNEL_ID") 8 | 9 | # Public callback URL 10 | CALLBACKURL="REPLACE_WITH_CALLBACK_URL" 11 | 12 | # Secret - must match ytnotify.php; should be reasonably hard to guess 13 | SECRET="REPLACE_WITH_UNIQUE_SECRET" 14 | 15 | ### ### ### ### ### ### 16 | 17 | 18 | 19 | for chid in "${CHANNELIDS[@]}" 20 | do 21 | echo "Subscribing to $chid..." 22 | 23 | curl -X POST https://pubsubhubbub.appspot.com/subscribe \ 24 | -d"hub.mode=subscribe" \ 25 | -d"hub.topic=https://www.youtube.com/xml/feeds/videos.xml?channel_id=$chid" \ 26 | -d"hub.callback=$CALLBACKURL" \ 27 | -d"hub.secret=$SECRET" \ 28 | -d"hub.verify=sync" 29 | done 30 | 31 | echo "Done." --------------------------------------------------------------------------------