Skip to content

Instantly share code, notes, and snippets.

@anon987654321
Created May 28, 2026 21:43
Show Gist options
  • Select an option

  • Save anon987654321/65e650bc608cc08270c6cb180704ebe5 to your computer and use it in GitHub Desktop.

Select an option

Save anon987654321/65e650bc608cc08270c6cb180704ebe5 to your computer and use it in GitHub Desktop.
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