Created
June 16, 2026 01:03
-
-
Save oliveira-andre/fcc3efd73d3399f72927190488db3e6d 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
| require 'mechanize' | |
| require 'byebug' | |
| require 'json' | |
| require 'open3' | |
| require 'base64' | |
| require 'net/http' | |
| require 'open-uri' | |
| require 'fileutils' | |
| require 'active_support/core_ext/string/inflections' | |
| require 'watir' | |
| require 'telegram/bot' | |
| TELEGRAM_KEY_TOKEN = ''.freeze | |
| TELEGRAM_CHAT_ID = ''.freeze | |
| # Only these chat ids may trigger commands through the listener. Defaults to the | |
| # configured TELEGRAM_CHAT_ID; add more ids as needed. | |
| TELEGRAM_ALLOWED_CHAT_IDS = [TELEGRAM_CHAT_ID].reject(&:empty?).map(&:to_i).freeze | |
| class Mp3Free | |
| attr_reader :url, :last_index | |
| HELP_TEXT = <<~TEXT.freeze | |
| Commands: | |
| /download <url> [count] β download a song, or a playlist (count = number of songs) | |
| /metadata <url> β fetch playlist/song metadata | |
| TEXT | |
| def initialize(url, last_index = nil, chat_id: TELEGRAM_CHAT_ID) | |
| @url = url | |
| @last_index = last_index&.to_i | |
| @chat_id = chat_id | |
| end | |
| # Long-polling listener (no webhook). Keeps the process alive and dispatches the | |
| # /download and /metadata commands sent through Telegram. | |
| def self.listen! | |
| if TELEGRAM_KEY_TOKEN.empty? | |
| abort 'Set TELEGRAM_KEY_TOKEN at the top of the file to use listen mode' | |
| end | |
| puts 'Listening for commands on Telegram (Ctrl-C to stop)...' | |
| Telegram::Bot::Client.run(TELEGRAM_KEY_TOKEN) do |bot| | |
| bot.listen { |message| handle_message(bot, message) } | |
| end | |
| end | |
| def self.handle_message(bot, message) | |
| return unless message.respond_to?(:text) && message.text | |
| chat_id = message.chat.id | |
| unless TELEGRAM_ALLOWED_CHAT_IDS.include?(chat_id) | |
| puts "Ignoring command from unauthorized chat_id=#{chat_id}" | |
| return | |
| end | |
| command, args = message.text.strip.split(/\s+/, 2) | |
| case command | |
| when '/start', '/help' | |
| bot.api.send_message(chat_id: chat_id, text: HELP_TEXT) | |
| when '/metadata' | |
| handle_metadata(bot, chat_id, args) | |
| when '/download' | |
| handle_download(bot, chat_id, args) | |
| end | |
| end | |
| def self.handle_metadata(bot, chat_id, url) | |
| return bot.api.send_message(chat_id: chat_id, text: 'Usage: /metadata <url>') unless url | |
| bot.api.send_message(chat_id: chat_id, text: "π Getting metadata: #{url}") | |
| new(url, chat_id: chat_id).write_metadata | |
| bot.api.send_message(chat_id: chat_id, text: 'Metadata got successfully') | |
| rescue StandardError => e | |
| puts "Metadata error: #{e.class}: #{e.message}" | |
| bot.api.send_message(chat_id: chat_id, text: "Metadata failed: #{url}") | |
| end | |
| def self.handle_download(bot, chat_id, args) | |
| url, last_index = args&.split(/\s+/, 2) | |
| return bot.api.send_message(chat_id: chat_id, text: 'Usage: /download <url> [count]') unless url | |
| bot.api.send_message(chat_id: chat_id, text: "βΆοΈ Starting download: #{url}") | |
| new(url, last_index, chat_id: chat_id).download_with_watir | |
| rescue StandardError => e | |
| puts "Download error: #{e.class}: #{e.message}" | |
| end | |
| def download | |
| agent = Mechanize.new | |
| agent.user_agent_alias = 'Mac Safari' | |
| # page = agent.get('https://youtubemp3free.com/en/') | |
| page = agent.get("https://youtubemp3free.com/index.php?link=#{url}") | |
| public_ip = Net::HTTP.get(URI('https://api.ipify.org')).strip | |
| i_param = Base64.strict_encode64(public_ip) | |
| metadata = self.metadata | |
| index = url.match(/&index=/)&.last | |
| id = if index | |
| metadata['entries'][index.to_i - 1]['id'] | |
| else | |
| metadata['id'] | |
| end | |
| slug = metadata['title'].parameterize | |
| first_url = "https://sern.info/youtube-to-mp3/#{slug}_#{id}.html?t=#{Time.now.to_i}&i=#{i_param}&utm_source=ytmp3&utm_medium=sync&utm_campaign=#{id}" | |
| page = agent.get(first_url) | |
| # The page fires an XHR that produces the cid. Mechanize doesn't run JS, so | |
| # we scan the HTML/JS for the endpoint and replay it ourselves. Cookies | |
| # persist via the agent; the Referer header is set explicitly so the server | |
| # treats this as in-context. | |
| xhr_endpoint = page.body[%r{["'](https?://[^"']+\.php[^"']*)["']}, 1] || | |
| page.body[%r{["'](/[^"']+\.php[^"']*)["']}, 1] | |
| xhr_endpoint = URI.join(first_url, xhr_endpoint).to_s if xhr_endpoint&.start_with?('/') | |
| agent.get(xhr_endpoint, [], first_url, { 'X-Requested-With' => 'XMLHttpRequest' }) if xhr_endpoint | |
| page = agent.get('https://sern.info/youtu.be', [], first_url) | |
| download_href = page.body[/download-href=["']([^"']+)["']/, 1] | |
| debugger | |
| puts download_href | |
| end | |
| def download_with_watir | |
| metadata = self.metadata | |
| index = url.split(/&index=/)&.last | |
| current_album = File.join('tmp', (metadata['title'] || 'downloads').gsub(%r{[/\\:*?"<>|]}, '_')) | |
| FileUtils.mkdir_p(current_album) | |
| if last_index | |
| current_index = index.to_i - 1 | |
| current_last_index = last_index - 1 | |
| (current_index..current_last_index).each do |i| | |
| if i > current_index | |
| id = metadata['entries'][i]['id'] | |
| new_url = url.gsub(/v=[^&]+/, "v=#{id}").gsub(/index=\d+/, "index=#{i + 1}") | |
| else | |
| new_url = url | |
| end | |
| with_retries(label: "track #{i + 1} (#{new_url})") do | |
| browser = Watir::Browser.new :chrome, headless: false | |
| begin | |
| browser.goto('https://youtubemp3free.com/en') | |
| browser.text_field(name: 'link').set(new_url) | |
| browser.button(type: 'submit').click | |
| browser.wait_until(timeout: 90) { |b| b.html.include?('download-href=') } | |
| sleep 5 | |
| title = browser.h1.text | |
| clickable = browser.a(id: 'download-url').href | |
| thumb_src = (browser.img(class: 'thumb').src if i.zero?) | |
| ensure | |
| browser.close | |
| end | |
| save_album_image(thumb_src, current_album) if thumb_src | |
| filename = File.join(current_album, "#{title.gsub(%r{[/\\:*?"<>|]}, '_')}.mp3") | |
| URI.parse(clickable).open do |source| | |
| File.open(filename, 'wb') { |f| IO.copy_stream(source, f) } | |
| end | |
| puts "Downloaded: #{filename}" | |
| notify("β Downloaded: <b>#{title}</b> (#{i + 1}/#{current_last_index + 1}). Ready for the next one.") | |
| end | |
| end | |
| notify("π Playlist finished: <b>#{metadata['title']}</b>. Ready to download another one.") | |
| else | |
| with_retries(label: url) do | |
| browser = Watir::Browser.new :chrome, headless: false | |
| begin | |
| browser.goto('https://youtubemp3free.com/en') | |
| browser.text_field(name: 'link').set(url) | |
| browser.button(type: 'submit').click | |
| browser.wait_until(timeout: 90) { |b| b.html.include?('download-href=') } | |
| sleep 5 | |
| title = browser.h1.text | |
| clickable = browser.a(id: 'download-url').href | |
| thumb_src = browser.img(class: 'thumb').src | |
| ensure | |
| browser.close | |
| end | |
| save_album_image(thumb_src, current_album) | |
| filename = File.join(current_album, "#{title.gsub(%r{[/\\:*?"<>|]}, '_')}.mp3") | |
| URI.parse(clickable).open do |source| | |
| File.open(filename, 'wb') { |f| IO.copy_stream(source, f) } | |
| end | |
| puts "Downloaded: #{filename}" | |
| notify("β Downloaded: <b>#{title}</b>. Ready to download another one.") | |
| end | |
| end | |
| end | |
| def metadata | |
| @metadata ||= if File.exist?(metadata_file) | |
| puts "Loading metadata from #{metadata_file}" | |
| JSON.parse(File.read(metadata_file)) | |
| else | |
| fetch_metadata | |
| end | |
| end | |
| def write_metadata | |
| FileUtils.mkdir_p(File.dirname(metadata_file)) | |
| data = fetch_metadata | |
| File.write(metadata_file, JSON.pretty_generate(data)) | |
| puts "Metadata written to #{metadata_file}" | |
| data | |
| end | |
| def metadata_file | |
| File.join('tmp', 'current_metadata.json') | |
| end | |
| private | |
| def with_retries(max: 5, label: nil) | |
| attempts = 0 | |
| begin | |
| yield | |
| rescue StandardError => e | |
| attempts += 1 | |
| if attempts < max | |
| puts "Attempt #{attempts} failed: #{e.message}. Retrying (#{attempts}/#{max - 1})..." | |
| retry | |
| end | |
| notify("β Download failed after #{max} attempts#{label ? " for #{label}" : ''}: #{e.message}") | |
| raise | |
| end | |
| end | |
| def notify(text) | |
| return if @chat_id.to_s.empty? || TELEGRAM_KEY_TOKEN.empty? | |
| telegram_bot.send_message(chat_id: @chat_id, text: text, parse_mode: 'HTML') | |
| rescue StandardError => e | |
| puts "Telegram notify failed: #{e.class}: #{e.message}" | |
| end | |
| def telegram_bot | |
| @telegram_bot ||= Telegram::Bot::Api.new(TELEGRAM_KEY_TOKEN) | |
| end | |
| def save_album_image(src, album_dir) | |
| ext = File.extname(URI.parse(src).path) | |
| ext = '.jpg' if ext.empty? | |
| path = File.join(album_dir, "album#{ext}") | |
| URI.parse(src).open { |source| File.open(path, 'wb') { |f| IO.copy_stream(source, f) } } | |
| puts "Saved album image: #{path}" | |
| end | |
| def fetch_metadata | |
| raise ArgumentError, "Invalid URL: #{url.inspect}" unless valid_http_url?(url) | |
| # `--` stops yt-dlp from treating a `-`-prefixed url as a flag (argv injection). | |
| stdout, _stderr, _status = Open3.capture3('yt-dlp', '-J', '--skip-download', '--', url) | |
| JSON.parse(stdout) | |
| end | |
| def valid_http_url?(value) | |
| return false if value.nil? || value.start_with?('-') | |
| URI.parse(value).is_a?(URI::HTTP) | |
| rescue URI::InvalidURIError | |
| false | |
| end | |
| end | |
| action = ARGV[0] | |
| case action | |
| when 'metadata' | |
| url = ARGV[1] | |
| abort 'Usage: ruby mp3free.rb metadata <url>' unless url | |
| Mp3Free.new(url).write_metadata | |
| when 'listen', nil | |
| # No args: stay alive and accept downloads through Telegram (no webhook). | |
| Mp3Free.listen! | |
| else | |
| # One-shot: ruby mp3free.rb <url> [last_index] | |
| url = ARGV[0] | |
| last_index = ARGV[1] | |
| Mp3Free.new(url, last_index).download_with_watir | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment