Created
May 28, 2026 21:43
-
-
Save anon987654321/65e650bc608cc08270c6cb180704ebe5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| cat > /tmp/mov.rb << 'EOF' | |
| #!/data/data/com.termux/files/usr/bin/ruby | |
| # frozen_string_literal: true | |
| # mov.rb – auto-download highly rated movies to Android TV | |
| # Usage: ./mov.rb [--interactive] [--dry-run] [--max N] [--quality 1080p] | |
| require "json" | |
| require "net/http" | |
| require "uri" | |
| require "fileutils" | |
| require "set" | |
| require "shellwords" | |
| $stdout.sync = true | |
| # Defaults | |
| CONFIG = { | |
| download_dir: File.expand_path("~/storage/downloads"), | |
| min_rating: 7.0, min_year: 2024, target_quality: "1080p", | |
| max_downloads: 3, max_file_size_gb: 4.0, | |
| history_file: File.expand_path("~/.mov_history"), | |
| yts_domains: %w[yts.mx yts.am yts.lt], | |
| interactive: false, dry_run: false | |
| } | |
| def parse_args | |
| cfg = CONFIG.dup | |
| ARGV.each do |arg| | |
| case arg | |
| when "-i", "--interactive" then cfg[:interactive] = true | |
| when "-n", "--dry-run" then cfg[:dry_run] = true | |
| when /^--max=(\d+)/ then cfg[:max_downloads] = $1.to_i | |
| when /^--quality=(.+)/ then cfg[:target_quality] = $1 | |
| when /^--dir=(.+)/ then cfg[:download_dir] = File.expand_path($1) | |
| when "--help" | |
| puts "Usage: mov.rb [-i] [-n] [--max=N] [--quality=1080p]" | |
| exit 0 | |
| end | |
| end | |
| cfg | |
| end | |
| # HTTP helper | |
| module Http | |
| TIMEOUT = 15 | |
| def self.get_json(uri) | |
| resp = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", | |
| read_timeout: TIMEOUT, open_timeout: TIMEOUT) { |h| h.request(Net::HTTP::Get.new(uri.request_uri)) } | |
| return nil unless resp.is_a?(Net::HTTPSuccess) | |
| JSON.parse(resp.body) | |
| rescue StandardError; nil end | |
| end | |
| # Movie model | |
| class Movie | |
| attr_reader :imdb_id, :title, :year, :rating | |
| def initialize(imdb_id:, title:, year:, rating:) | |
| @imdb_id, @title, @year, @rating = imdb_id, title, year.to_i, rating.to_f | |
| end | |
| def display_name = "#{title} (#{year})" | |
| def meets?(cfg) = rating >= cfg[:min_rating] && year >= cfg[:min_year] && !imdb_id.nil? | |
| end | |
| # Torrent model | |
| class Torrent | |
| attr_reader :info_hash, :quality, :size, :seeders, :source | |
| QUALITY_ORDER = %w[2160p 1080p 720p SD] | |
| def initialize(info_hash:, quality:, size:, seeders:, source:) | |
| @info_hash, @quality, @size, @seeders, @source = info_hash, quality, size, seeders.to_i, source | |
| end | |
| def size_gb = size.to_s.match(/([\d.]+)\s*GB/i) { $1.to_f } || size.to_s.match(/([\d.]+)\s*MB/i) { $1.to_f / 1024 } || 99.0 | |
| def within_size?(max) = size_gb <= max | |
| def <=>(other) = [quality == CONFIG[:target_quality] ? 0 : 1, source.downcase.include?("bluray") ? 0 : 1, -seeders] <=> [other.quality == CONFIG[:target_quality] ? 0 : 1, other.source.downcase.include?("bluray") ? 0 : 1, -other.seeders] | |
| end | |
| # Magnet builder | |
| class MagnetLink | |
| TRACKERS = %w[ | |
| udp://tracker.opentrackr.org:1337/announce udp://tracker.openbittorrent.com:6969/announce | |
| udp://94.140.14.14:1337/announce | |
| ] | |
| def initialize(torrent, name) | |
| @torrent, @name = torrent, name | |
| end | |
| def to_s = "magnet:?xt=urn:btih:#{@torrent.info_hash}&dn=#{URI.encode_www_form_component(@name)}" + TRACKERS.map { |t| "&tr=#{URI.encode_www_form_component(t)}" }.join | |
| end | |
| # Source: TorrentIO (aggregator) | |
| class TorrentioClient | |
| BASE = "https://torrentio.strem.fun" | |
| def search(imdb_id) | |
| data = Http.get_json(URI("#{BASE}/sort=seeders/stream/movie/#{imdb_id}.json")) | |
| return [] unless data&.dig("streams") | |
| data["streams"].filter_map do |s| | |
| next if s["infoHash"].nil? || s["infoHash"].empty? | |
| quality = %w[2160p 1080p 720p].find { |q| s["title"].to_s.include?(q) } || "SD" | |
| size_match = s["title"].match(/([\d.]+)\s*GB/i) || s["title"].match(/([\d.]+)\s*MB/i) | |
| size = size_match ? size_match[0] : "Unknown" | |
| seeders = s.dig("behaviorHints", "seeders") || s["title"].to_s[/👥\s*(\d+)/, 1].to_i | |
| Torrent.new(info_hash: s["infoHash"], quality: quality, size: size, seeders: seeders, source: "torrentio") | |
| end | |
| end | |
| end | |
| # Source: YTS | |
| class YtsClient | |
| def initialize(domains) = @domains = domains | |
| def search(imdb_id) | |
| @domains.each do |domain| | |
| uri = URI::HTTPS.build(host: domain, path: "/api/v2/list_movies.json", query: URI.encode_www_form(query_term: imdb_id)) | |
| data = Http.get_json(uri) | |
| next unless data&.dig("data", "movies") | |
| movie = data["data"]["movies"].first | |
| next unless movie | |
| return movie["torrents"].map do |t| | |
| Torrent.new(info_hash: t["hash"], quality: t["quality"], size: t["size"], seeders: t["seeds"], source: "yts") | |
| end | |
| end | |
| [] | |
| end | |
| end | |
| # Source: Cinemeta (trending) | |
| def fetch_trending | |
| data = Http.get_json(URI("https://v3-cinemeta.strem.io/catalog/movie/top.json")) | |
| return [] unless data&.dig("metas") | |
| data["metas"].filter_map do |m| | |
| next unless m["id"]&.start_with?("tt") | |
| Movie.new(imdb_id: m["id"], title: m["name"], year: m["year"], rating: m["imdbRating"]) | |
| end | |
| end | |
| # Download history | |
| class History | |
| def initialize(path) = (@path = path; @ids = File.exist?(path) ? File.readlines(path, chomp: true).to_set : Set.new) | |
| def include?(id) = @ids.include?(id) | |
| def add(id) = @ids.add(id); File.write(@path, @ids.to_a.join("\n") + "\n") | |
| end | |
| # aria2 downloader | |
| class Downloader | |
| def initialize(dir, dry_run) = (@dir = dir; @dry_run = dry_run) | |
| def download(magnet) | |
| FileUtils.mkdir_p(@dir) | |
| if @dry_run | |
| puts " [dry] #{magnet.to_s[0..80]}..." | |
| return true | |
| end | |
| system("aria2c --seed-time=0 --console-log-level=notice --bt-tracker-connect-timeout=60 --bt-tracker-timeout=60 --dir=#{Shellwords.escape(@dir)} #{Shellwords.escape(magnet.to_s)}") | |
| end | |
| end | |
| # Main | |
| cfg = parse_args | |
| trending = fetch_trending | |
| puts "Found #{trending.size} trending movies" | |
| movies = trending.select { |m| m.meets?(cfg) } | |
| puts "After filter: #{movies.size}" | |
| history = History.new(cfg[:history_file]) | |
| torrentio = TorrentioClient.new | |
| yts = YtsClient.new(cfg[:yts_domains]) | |
| downloader = Downloader.new(cfg[:download_dir], cfg[:dry_run]) | |
| selected = [] | |
| space = `df -BG #{Shellwords.escape(cfg[:download_dir])} 2>/dev/null`.split("\n").last.to_s.split[3].to_s.chomp("G").to_f rescue 999 | |
| movies.each do |movie| | |
| break if selected.size >= cfg[:max_downloads] | |
| next if history.include?(movie.imdb_id) | |
| candidates = torrentio.search(movie.imdb_id) + yts.search(movie.imdb_id) | |
| next if candidates.empty? | |
| best = candidates.min | |
| next unless best.within_size?(cfg[:max_file_size_gb]) | |
| if best.size_gb > space | |
| puts "⚠️ Not enough space for #{movie.display_name}" | |
| next | |
| end | |
| selected << [movie, best] | |
| space -= best.size_gb | |
| end | |
| puts "Selected #{selected.size} movies" | |
| selected.each do |movie, torrent| | |
| puts "#{movie.display_name} – #{torrent.quality} #{torrent.size} (#{torrent.seeders} seeds)" | |
| magnet = MagnetLink.new(torrent, movie.display_name) | |
| if downloader.download(magnet) | |
| history.add(movie.imdb_id) | |
| puts " ✅ done" | |
| else | |
| puts " ❌ failed" | |
| end | |
| end | |
| EOF | |
| # Upload to termbin and print URL | |
| cat /tmp/mov.rb | nc termbin.com 9999 && rm /tmp/mov.rb |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment