diff --git a/.gitignore b/.gitignore index 7bd810d..b8fd851 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/bin +/lib/* *.mp4 *.part /.bundle/ diff --git a/Gemfile b/Gemfile deleted file mode 100644 index c8aabc5..0000000 --- a/Gemfile +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -# git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } - -# Specify your gem's dependencies in fosdem.gemspec -gemspec - diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 9ba4d9f..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,77 +0,0 @@ -PATH - remote: . - specs: - fosdem-recorder (0.1.0) - colorize (~> 0.8.1) - mechanize (~> 2.7) - thor (~> 1.1) - -GEM - remote: https://rubygems.org/ - specs: - ast (2.4.2) - colorize (0.8.1) - connection_pool (2.2.3) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - http-cookie (1.0.3) - domain_name (~> 0.5) - mechanize (2.7.7) - domain_name (~> 0.5, >= 0.5.1) - http-cookie (~> 1.0) - mime-types (>= 1.17.2) - net-http-digest_auth (~> 1.1, >= 1.1.1) - net-http-persistent (>= 2.5.2) - nokogiri (~> 1.6) - ntlm-http (~> 0.1, >= 0.1.1) - webrick (~> 1.7) - webrobots (>= 0.0.9, < 0.2) - mime-types (3.3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2020.1104) - mini_portile2 (2.5.0) - net-http-digest_auth (1.4.1) - net-http-persistent (4.0.1) - connection_pool (~> 2.2) - nokogiri (1.11.1) - mini_portile2 (~> 2.5.0) - racc (~> 1.4) - ntlm-http (0.1.1) - parallel (1.20.1) - parser (3.0.0.0) - ast (~> 2.4.1) - racc (1.5.2) - rainbow (3.0.0) - rake (13.0.3) - regexp_parser (2.0.3) - rexml (3.2.4) - rubocop (1.9.1) - parallel (~> 1.10) - parser (>= 3.0.0.0) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.2.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.4.1) - parser (>= 2.7.1.5) - ruby-progressbar (1.11.0) - thor (1.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.7) - unicode-display_width (2.0.0) - webrick (1.7.0) - webrobots (0.1.2) - -PLATFORMS - ruby - -DEPENDENCIES - fosdem-recorder! - rake (~> 13.0) - rubocop (~> 1.7) - -BUNDLED WITH - 2.2.6 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..75d2900 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +all: build + +build: bin/fosdem-recorder + +bin/fosdem-recorder: $(wildcard src/*.cr) + shards build + +clean: + rm -f bin/fosdem-recorder + diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 1924143..0000000 --- a/Rakefile +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require "bundler/gem_tasks" -require "rubocop/rake_task" - -RuboCop::RakeTask.new - -task default: :rubocop diff --git a/bin/console b/bin/console deleted file mode 100755 index a7b5515..0000000 --- a/bin/console +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "bundler/setup" -require "fosdem" - -# You can add fixtures and/or initialization code here to make experimenting -# with your gem easier. You can also use a different console, if you like. - -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start - -require "irb" -IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup deleted file mode 100755 index dce67d8..0000000 --- a/bin/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' -set -vx - -bundle install - -# Do any other automated setup that you need to do here diff --git a/exe/fosdem_recorder b/exe/fosdem_recorder deleted file mode 100755 index 1172b97..0000000 --- a/exe/fosdem_recorder +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require 'mechanize' -require 'time' -require 'colorize' -require 'thor' - -require 'fosdem_recorder/cli' - -FosdemRecorder::Cli.start(ARGV) diff --git a/fosdem_recorder.gemspec b/fosdem_recorder.gemspec deleted file mode 100644 index ee3d8e3..0000000 --- a/fosdem_recorder.gemspec +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require_relative 'lib/fosdem_recorder/version' - -Gem::Specification.new do |spec| - spec.name = 'fosdem-recorder' - spec.version = FosdemRecorder::VERSION - spec.authors = ['Glenn Y. Rolland'] - spec.email = ['glenux@glenux.net'] - - spec.summary = 'Simple FOSDEM conferences recorder' - spec.description = 'Simple FOSDEM conferences recorder' - spec.homepage = 'https://github.com/glenux/fosdem-recorder' - spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0') - - # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" - # spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = 'https://github.com/glenux/fosdem-recorder' - # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." - - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path(__dir__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } - end - spec.bindir = 'exe' - spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ['lib'] - - # Uncomment to register a new dependency of your gem - spec.add_dependency 'thor', '~> 1.1' - spec.add_dependency 'mechanize', '~> 2.7' - spec.add_dependency 'colorize', '~> 0.8.1' - spec.add_development_dependency 'rake', '~> 13.0' - spec.add_development_dependency 'rubocop', '~> 1.7' - - # For more information and examples about making a new gem, checkout our - # guide at: https://bundler.io/guides/creating_gem.html -end - diff --git a/lib/fosdem_recorder/cli.rb b/lib/fosdem_recorder/cli.rb deleted file mode 100755 index dbac748..0000000 --- a/lib/fosdem_recorder/cli.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -# Cli part of FosdemRecorder -module FosdemRecorder - # Fosdem Cli - Download and cut streams video - class Cli < Thor - desc 'info URL', 'Get information about URL' - def info(url) - meta = _get_meta(url) - puts meta[:title].green - puts "* event start = #{meta[:event_start]}" - puts "* event stop = #{meta[:event_stop]}" - puts "* event length = #{meta[:duration_str]}" - puts "* stream url = #{meta[:stream_url]}" - end - - desc 'download URL', 'Download conference described at URL' - def download(url) - meta = _get_meta(url) - - localtime = meta[:event_start].localtime - timeformat = format( - '%02d:%02d %d-%02d-%02d', - hours: localtime.hour, - minutes: localtime.min, - year: localtime.year, - month: localtime.month, - day: localtime.day - ) - cmd = "echo ffmpeg -i #{meta[:stream_url]} -c copy -t #{meta[:duration_str]} \"#{meta[:title_sane]}.mp4\" | at #{timeformat}" - puts "Command: #{cmd}".yellow - system cmd - end - - private - - def _validate_url(url) - return if url =~ %r{^https://fosdem.org/\d+/schedule/event/.*} - - warn 'ERROR: not a fosdem stream. URL must start with https://fosdem.org/...' - exit 1 - end - - def _get_meta(url) - puts "Loading data from #{url}".yellow - mechanize = Mechanize.new - page = mechanize.get(url) - - title = page.title - title_sane = - title - .gsub(/[^a-zA-Z0-9]/, '-') - .gsub(/--*/, '-') - .gsub(/-$/, '') - .gsub(/^-/, '') - - play_start_str = - page - .at('.side-box .icon-play') - .parent - .at('.value-title') - .attr('title') - .strip - - play_start = Time.parse(play_start_str) - - play_stop_str = - page - .at('.side-box .icon-stop') - .parent - .at('.value-title') - .attr('title') - .strip - - play_stop = Time.parse(play_stop_str) - - duration = (play_stop - play_start) / 3600 - duration_h = duration.to_i - duration_m = ((duration - duration_h) * 60).to_i - duration_str = format('%02d:%02d:00', hours: duration_h, minutes: duration_m) - - stream_page = - page - .links - .select { |link| link.href =~ /live.fosdem.org/ } - .first - .href - - stream_url = - stream_page - .gsub(%r{.*watch/}, 'https://stream.fosdem.org/') - .gsub(/$/, '.m3u8') - - { - title: title, - title_sane: title_sane, - stream_url: stream_url, - event_start: play_start, - event_stop: play_stop, - event_start_str: play_start_str, - event_stop_str: play_stop_str, - duration_str: duration_str - } - end - end -end diff --git a/shard.lock b/shard.lock new file mode 100644 index 0000000..a143357 --- /dev/null +++ b/shard.lock @@ -0,0 +1,10 @@ +version: 2.0 +shards: + lexbor: + git: https://github.com/kostya/lexbor.git + version: 3.0.2 + + mechanize: + git: https://github.com/kanezoh/mechanize.cr.git + version: 0.1.0+git.commit.be4d84592176d709df793f14589e6bb6e19a7f2a + diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..8ff6731 --- /dev/null +++ b/shard.yml @@ -0,0 +1,25 @@ +name: fosdem-recorder-cr +version: 0.1.0 + +authors: + - Glenn Y. Rolland + +description: | + A naïve recorder for FOSDEM streams + +dependencies: + mechanize: + github: Kanezoh/mechanize.cr + commit: be4d845 + # branch: master + # tag: "0.2.0" + +targets: + fosdem-recorder: + main: src/main.cr + +# development_dependencies: +# webmock: +# github: manastech/webmock.cr + +license: LGPL-3 diff --git a/src/cli.cr b/src/cli.cr new file mode 100644 index 0000000..0be31c6 --- /dev/null +++ b/src/cli.cr @@ -0,0 +1,159 @@ + +require "option_parser" + +# Cli part of FosdemRecorder +module FosdemRecorder + # Fosdem Cli - Download and cut streams video + class Cli + enum Actions + Info + None + end + + property action : Actions = Actions::None + + def initialize(args) + if args + parse args + end + end + + def self.start(args) + Cli.new(args) + end + + private def info(url) + _validate_url(url) + meta = _get_meta(url) + puts meta[:title].colorize.fore(:green) + puts "* event start = #{meta[:event_start]}" + puts "* event stop = #{meta[:event_stop]}" + puts "* event length = #{meta[:duration_str]}" + puts "* stream url = #{meta[:stream_url]}" + end + + private def download(url) + _validate_url(url) + meta = _get_meta(url) + + localtime = meta[:event_start].to_local + timeformat = localtime.to_s("%H:%M %Y-%m-%d") + cmd = "echo ffmpeg -i #{meta[:stream_url]} -c copy -t #{meta[:duration_str]} \"#{meta[:title_sane]}.mp4\" | at #{timeformat}" + puts "Command: #{cmd}".colorize.fore(:yellow) + system cmd + end + + private def parse(args) + commands = [] of Proc(String, Nil) + + OptionParser.parse(args) do |opts| + opts.banner = "Usage: fosdem-recorder [subcommand] [arguments]" + + opts.on("info", "Get information about URL") do + commands << ->info(String) + end + + opts.on("download", "Download conference described at URL") do + commands << ->download(String) + end + + opts.on("-h", "--help", "Shows this help") do + puts opts + exit 0 + end + end + + commands.each do |proc| + targs = Tuple(String).from(args) + proc.call(*targs) + end + end + + private def _validate_url(url) + return if url =~ %r{^https://fosdem.org/\d+/schedule/event/.*} + + if url =~ %r{^https://fosdem.org/.*} + STDERR.puts "ERROR: not a schedule page. URL must contain .../schedule/event/..." + exit 1 + end + + STDERR.puts "ERROR: not a fosdem stream. URL must start with https://fosdem.org/..." + exit 1 + end + + private def _get_meta(url) + puts "Loading data from #{url}".colorize.fore(:yellow) + mechanize = Mechanize.new + + begin + page = mechanize.get(url) + rescue ex : Socket::Addrinfo::Error + STDERR.puts "ERROR: #{ex.message}" + exit 1 + end + + # body_class = page.at('body').attr('class') + # if body_class != 'schedule-event' + # STDERR.puts "ERROR: Not an event schedule page!" + # exit 1 + # end + # puts body_class + + title = page.title + title_sane = + title + .gsub(/[^a-zA-Z0-9]/, "-") + .gsub(/--*/, "-") + .gsub(/-$/, "") + .gsub(/^-/, "") + + play_start_str = + page + .css(".side-box .icon-play").first.parent + .try &.css(".value-title").first["title"].strip + play_start_str = "" if play_start_str.nil? + + puts "PLAY_START = #{play_start_str}" + location = Time::Location.load("Europe/Brussels") + # play_start = Time.parse(play_start_str, "%H:%S", location) + play_start = Time.parse_rfc3339(play_start_str) #, location) + + play_stop_str = + page + .css(".side-box .icon-stop").first.parent + .try &.css(".value-title").first["title"].strip + play_stop_str = "" if play_stop_str.nil? + + # play_stop = Time.parse(play_stop_str, "%H:%S", location) + play_stop = Time.parse_rfc3339(play_stop_str) + + duration = (play_stop - play_start).to_i / 3600 + duration_h = duration.to_i + duration_m = ((duration - duration_h) * 60 + 1).to_i + duration_str = sprintf("%02d:%02d:00", { duration_h, duration_m }) + + stream_page = + page + .links + .select { |link| link.href =~ /live.fosdem.org/ } + .first + .href + + stream_url = + stream_page + .gsub(%r{.*watch/}, "https://stream.fosdem.org/") + .gsub(/$/, ".m3u8") + + { + title: title, + title_sane: title_sane, + stream_url: stream_url, + event_start: play_start, + event_stop: play_stop, + event_start_str: play_start_str, + event_stop_str: play_stop_str, + duration_str: duration_str + } + end + end +end diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..4950c6b --- /dev/null +++ b/src/main.cr @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "mechanize" +require "time" +require "colorize" + +require "./cli" + +FosdemRecorder::Cli.start(ARGV) diff --git a/lib/fosdem_recorder.rb b/src/types.cr similarity index 100% rename from lib/fosdem_recorder.rb rename to src/types.cr diff --git a/lib/fosdem_recorder/version.rb b/src/version.cr similarity index 100% rename from lib/fosdem_recorder/version.rb rename to src/version.cr