A Twitter bot that tweets random frames from the Computer Chronicles TV show https://twitter.com/chronicles_bot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cc.rb 3.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. require 'dotenv/load'
  2. require 'google/apis/youtube_v3'
  3. require 'json'
  4. require 'open-uri'
  5. require 'twitter'
  6. Dotenv.load
  7. YOUTUBE_API_KEY = ENV["YOUTUBE_API_KEY"]
  8. TWITTER_CONSUMER_KEY = ENV["TWITTER_CONSUMER_KEY"]
  9. TWITTER_CONSUMER_SECRET = ENV["TWITTER_CONSUMER_SECRET"]
  10. TWITTER_ACCESS_TOKEN = ENV["TWITTER_ACCESS_TOKEN"]
  11. TWITTER_ACCESS_TOKEN_SECRET = ENV["TWITTER_ACCESS_TOKEN_SECRET"]
  12. YOUTUBE_DL_PATH = `which youtube-dl`.chomp
  13. FFMPEG_PATH = `which ffmpeg`.chomp
  14. FFPROBE_PATH = `which ffprobe`.chomp
  15. throw "YOUTUBE_API_KEY is required" unless YOUTUBE_API_KEY
  16. throw "TWITTER_CONSUMER_KEY is required" unless TWITTER_CONSUMER_KEY
  17. throw "TWITTER_CONSUMER_SECRET is required" unless TWITTER_CONSUMER_SECRET
  18. throw "TWITTER_ACCESS_TOKEN is required" unless TWITTER_ACCESS_TOKEN
  19. throw "TWITTER_ACCESS_TOKEN_SECRET is required" unless TWITTER_ACCESS_TOKEN_SECRET
  20. throw "Can't find youtube-dl" if YOUTUBE_DL_PATH.empty?
  21. throw "Can't find ffprobe" if FFPROBE_PATH.empty?
  22. throw "Can't find ffmpeg" if FFMPEG_PATH.empty?
  23. class YouTubeService
  24. @@RESULTS_PER_PAGE = 50.0
  25. @@PLAYLIST_OPTIONS = {
  26. fields: 'items/snippet/resourceId/videoId,nextPageToken,page_info',
  27. max_results: @@RESULTS_PER_PAGE,
  28. }
  29. def initialize
  30. @youtube = Google::Apis::YoutubeV3::YouTubeService.new
  31. @youtube.key = YOUTUBE_API_KEY
  32. end
  33. def fetch_random_frame_from_random_video
  34. video_id = fetch_random_video_from_channel('ComputerChroniclesYT')
  35. video_path = download_video(video_id)
  36. frame_path = extract_frame(video_path)
  37. delete_file(video_path)
  38. frame_path
  39. end
  40. def fetch_random_video_from_channel(channel_name)
  41. uploads_playlist_id = fetch_channel_uploads_playlist(channel_name)
  42. response = fetch_playlist_videos(uploads_playlist_id)
  43. extract_random_video(uploads_playlist_id, response)
  44. end
  45. def fetch_channel_uploads_playlist(channel_name)
  46. channel = @youtube.list_channels('contentDetails', for_username: channel_name)
  47. throw "No channel found" unless channel.items.first
  48. channel.items.first.content_details.related_playlists.uploads
  49. end
  50. def fetch_playlist_videos(playlist_id)
  51. @youtube.list_playlist_items('snippet', @@PLAYLIST_OPTIONS.merge(playlist_id: playlist_id))
  52. end
  53. def extract_random_video(playlist_id, response)
  54. number_of_videos = response.page_info.total_results
  55. number_of_pages = (number_of_videos / @@RESULTS_PER_PAGE).ceil
  56. random_page_number = rand(1..number_of_pages)
  57. (1..random_page_number).each do
  58. response = @youtube.list_playlist_items(
  59. 'snippet',
  60. @@PLAYLIST_OPTIONS.merge(
  61. page_token: response.next_page_token,
  62. playlist_id: playlist_id
  63. )
  64. )
  65. end
  66. response.items.sample.snippet.resource_id.video_id
  67. end
  68. def download_video(video_id)
  69. output_path = "/tmp/video.mp4"
  70. `#{YOUTUBE_DL_PATH} -f mp4 -o /tmp/video.mp4 #{video_id}`
  71. output_path
  72. end
  73. def extract_frame(video_path)
  74. output_path = "/tmp/frame.png"
  75. length = get_video_length(video_path)
  76. random_second = rand(0..length)
  77. `#{FFMPEG_PATH} -v error -ss #{random_second} -i #{video_path} -frames 1 -y #{output_path}`
  78. output_path
  79. end
  80. def get_video_length(video_path)
  81. `#{FFPROBE_PATH} -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 #{video_path}`.chomp.to_i
  82. end
  83. def delete_file(path)
  84. `rm #{path}`
  85. end
  86. end
  87. class TwitterService
  88. def initialize
  89. @twitter = Twitter::REST::Client.new do |config|
  90. config.consumer_key = TWITTER_CONSUMER_KEY
  91. config.consumer_secret = TWITTER_CONSUMER_SECRET
  92. config.access_token = TWITTER_ACCESS_TOKEN
  93. config.access_token_secret = TWITTER_ACCESS_TOKEN_SECRET
  94. end
  95. end
  96. def tweet_image(filepath)
  97. @twitter.update_with_media(String.new, File.new(filepath))
  98. end
  99. end
  100. youtube = YouTubeService.new
  101. filepath = youtube.fetch_random_frame_from_random_video
  102. TwitterService.new.tweet_image(filepath)
  103. youtube.delete_file(filepath)