Compare commits
4 commits
e33041a4fb
...
efe00b5923
Author | SHA1 | Date | |
---|---|---|---|
efe00b5923 | |||
efb76d3e19 | |||
d52d116e8c | |||
3c2c7fc3de |
11 changed files with 334 additions and 136 deletions
26
LICENSE
Normal file
26
LICENSE
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Copyright (c) The Regents of the University of California.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. Neither the name of the University nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||||
|
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||||
|
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGE.
|
75
README.md
75
README.md
|
@ -2,51 +2,70 @@
|
||||||
# FOSDEM Recorder
|
# FOSDEM Recorder
|
||||||
|
|
||||||
A tool to schedule FOSDEM recordings and help you record the 5+ talks you
|
A tool to schedule FOSDEM recordings and help you record the 5+ talks you
|
||||||
wanted to attend at the same time. Only for the impatient who can't wait for
|
wanted to attend at the same time.
|
||||||
the official videos to be made available.
|
|
||||||
|
Only for the impatient who can't wait for the official videos to be made
|
||||||
|
available (usually this happens a few days after the event).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install project dependencies
|
If you have Crystal 1.7+ installed on your system:
|
||||||
|
|
||||||
$ bundle install
|
shards install
|
||||||
|
shards build
|
||||||
|
sudo cp bin/fosdem-recorder /usr/local/bin/fosdem-recorder
|
||||||
|
|
||||||
|
If you don't have crystal, you can also build it with docker:
|
||||||
|
|
||||||
|
docker run --name fosdem-recorder \
|
||||||
|
-v $(pwd):/app crystallang/crystal:1.7.1 \
|
||||||
|
sh -c "cd /app && shards install && shards build"
|
||||||
|
docker cp fosdem-recorder:/app/bin/fosdem-recorder fosdem-recorder
|
||||||
|
docker rm fosdem-recorder
|
||||||
|
mv fosdem-recorder /usr/local/bin/fosdem-recorder
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Get information about given URL
|
### Get information about given URL
|
||||||
|
|
||||||
$ fosdem_recorder info URL
|
Syntax:
|
||||||
|
|
||||||
|
|
||||||
Schedule video download for given URL
|
|
||||||
|
|
||||||
$ fosdem_recorder download URL
|
|
||||||
|
|
||||||
|
|
||||||
Real example
|
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ bundle exec fosdem-recorder info https://fosdem.org/2021/schedule/event/sca_weclome/
|
$ fosdem-recorder info EVENT_URL
|
||||||
* title = FOSDEM 2021 - Software Composition Analysis Devroom Welcome
|
|
||||||
* start = 14:00
|
|
||||||
* stop = 14:05
|
|
||||||
* diff = 00:05:00
|
|
||||||
* url = https://stream.fosdem.org/dcomposition.m3u8
|
|
||||||
|
|
||||||
$ bundle exec fosdem-recorder info https://fosdem.org/2021/schedule/event/sca_weclome/
|
|
||||||
[... schedules the download with 'at'... ]
|
|
||||||
[... downloads the file with 'ffmpeg'... ]
|
|
||||||
[... a MP4 file will be created once the video is downloaded ...]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Real example:
|
||||||
|
|
||||||
## Development
|
```shell-session
|
||||||
|
$ fosdem-recorder info https://fosdem.org/2023/schedule/event/nasa/
|
||||||
|
Loading data from https://fosdem.org/2023/schedule/event/nasa/
|
||||||
|
FOSDEM 2023 - Open Source Software at NASA
|
||||||
|
* event start = 2023-02-05 17:00:00 +01:00
|
||||||
|
* event stop = 2023-02-05 17:50:00 +01:00
|
||||||
|
* event length = 00:51:00 (from now: 01:20:00)
|
||||||
|
* stream url = https://stream.fosdem.org/janson.m3u8
|
||||||
|
```
|
||||||
|
|
||||||
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
### Schedule video download for given URL
|
||||||
|
|
||||||
|
Syntax:
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ fosdem-recorder download EVENT_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Real example:
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ fosdem-recorder download https://fosdem.org/2023/schedule/event/nasa/
|
||||||
|
Loading data from https://fosdem.org/2023/schedule/event/nasa/
|
||||||
|
Command: echo ffmpeg -i https://stream.fosdem.org/janson.m3u8 -c copy -bsf:a aac_adtstoasc -movflags frag_keyframe+empty_moov+default_base_moof+faststart -t 01:19:00 "FOSDEM-2023-Open-Source-Software-at-NASA.mp4" | at 17:00 2023-02-05
|
||||||
|
warning: commands will be executed using /bin/sh
|
||||||
|
job 635 at Sun Feb 5 17:00:00 2023
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Bug reports and pull requests are welcome on GitHub at https://github.com/glenux/fosdem-recorder.
|
Send bug reports and patches by email to <opensource@glenux.net>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ version: 2.0
|
||||||
shards:
|
shards:
|
||||||
lexbor:
|
lexbor:
|
||||||
git: https://github.com/kostya/lexbor.git
|
git: https://github.com/kostya/lexbor.git
|
||||||
version: 3.0.2
|
version: 3.0.4
|
||||||
|
|
||||||
mechanize:
|
mechanize:
|
||||||
git: https://github.com/kanezoh/mechanize.cr.git
|
git: https://github.com/kanezoh/mechanize.cr.git
|
||||||
|
|
118
src/cli.cr
118
src/cli.cr
|
@ -1,6 +1,11 @@
|
||||||
|
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
|
|
||||||
|
require "./duration"
|
||||||
|
require "./meta_data"
|
||||||
|
require "./controllers/info_controller"
|
||||||
|
require "./controllers/download_controller"
|
||||||
|
|
||||||
# Cli part of FosdemRecorder
|
# Cli part of FosdemRecorder
|
||||||
module FosdemRecorder
|
module FosdemRecorder
|
||||||
# Fosdem Cli - Download and cut streams video
|
# Fosdem Cli - Download and cut streams video
|
||||||
|
@ -22,26 +27,7 @@ module FosdemRecorder
|
||||||
Cli.new(args)
|
Cli.new(args)
|
||||||
end
|
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)
|
private def parse(args)
|
||||||
commands = [] of Proc(String, Nil)
|
commands = [] of Proc(String, Nil)
|
||||||
|
@ -49,12 +35,16 @@ module FosdemRecorder
|
||||||
OptionParser.parse(args) do |opts|
|
OptionParser.parse(args) do |opts|
|
||||||
opts.banner = "Usage: fosdem-recorder [subcommand] [arguments]"
|
opts.banner = "Usage: fosdem-recorder [subcommand] [arguments]"
|
||||||
|
|
||||||
|
# opts.on("update-cache", "Fetch schedule information") do
|
||||||
|
# commands << ->InfoController.process(String)
|
||||||
|
# end
|
||||||
|
|
||||||
opts.on("info", "Get information about URL") do
|
opts.on("info", "Get information about URL") do
|
||||||
commands << ->info(String)
|
commands << ->InfoController.process(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
opts.on("download", "Download conference described at URL") do
|
opts.on("download", "Download conference described at URL") do
|
||||||
commands << ->download(String)
|
commands << ->DownloadController.process(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
opts.on("-h", "--help", "Shows this help") do
|
opts.on("-h", "--help", "Shows this help") do
|
||||||
|
@ -69,91 +59,5 @@ module FosdemRecorder
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
6
src/controllers/base_controller.cr
Normal file
6
src/controllers/base_controller.cr
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
module FosdemRecorder
|
||||||
|
class BaseController
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
58
src/controllers/download_controller.cr
Normal file
58
src/controllers/download_controller.cr
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
require "./base_controller"
|
||||||
|
require "../utils/url_validator"
|
||||||
|
|
||||||
|
module FosdemRecorder
|
||||||
|
class DownloadController < BaseController
|
||||||
|
def self.process(url)
|
||||||
|
UrlValidator.validate_event! url
|
||||||
|
meta = EventPage.get_meta(url)
|
||||||
|
|
||||||
|
now = Time.local #Time::Location.load("Europe/Brussels")
|
||||||
|
|
||||||
|
event_start = meta.event_start
|
||||||
|
raise "Event start is missing!" if event_start.nil?
|
||||||
|
|
||||||
|
event_stop = meta.event_stop
|
||||||
|
raise "Event stop is missing!" if event_stop.nil?
|
||||||
|
|
||||||
|
event_start_localtime = event_start.to_local
|
||||||
|
postpone_download = (event_start_localtime >= now)
|
||||||
|
|
||||||
|
# compute remaining duration when needed
|
||||||
|
duration = Duration.new(start: event_start, stop: event_stop)
|
||||||
|
remaining_duration = duration.from_now
|
||||||
|
timeformat = event_start_localtime.to_s("%H:%M %Y-%m-%d")
|
||||||
|
|
||||||
|
# FIXME: mark the file as partial
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
|
||||||
|
# First the stream URL
|
||||||
|
"-i", meta.stream_url,
|
||||||
|
|
||||||
|
# Then the codec (simple copy)
|
||||||
|
"-c", "copy",
|
||||||
|
|
||||||
|
# Fix malformed AAC bitstream when detected
|
||||||
|
"-bsf:a", "aac_adtstoasc",
|
||||||
|
|
||||||
|
# Make the stream playable as we download it
|
||||||
|
"-movflags", "frag_keyframe+empty_moov+default_base_moof+faststart",
|
||||||
|
|
||||||
|
# Set record duration
|
||||||
|
"-t", remaining_duration.to_s,
|
||||||
|
|
||||||
|
# Set output filename
|
||||||
|
"\"#{meta.title_sanitized}.mp4\""
|
||||||
|
].join(" ")
|
||||||
|
|
||||||
|
if postpone_download
|
||||||
|
cmd = "echo #{cmd} | at #{timeformat}"
|
||||||
|
else
|
||||||
|
cmd = "echo #{cmd} | at now"
|
||||||
|
end
|
||||||
|
puts "Command: #{cmd}".colorize.fore(:yellow)
|
||||||
|
system cmd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
src/controllers/info_controller.cr
Normal file
31
src/controllers/info_controller.cr
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
require "./base_controller"
|
||||||
|
require "../utils/url_validator"
|
||||||
|
require "../event_page"
|
||||||
|
|
||||||
|
module FosdemRecorder
|
||||||
|
class InfoController < BaseController
|
||||||
|
def self.process(url)
|
||||||
|
UrlValidator.validate_event! url
|
||||||
|
meta = EventPage.get_meta(url)
|
||||||
|
|
||||||
|
puts meta.title.colorize.fore(:green)
|
||||||
|
|
||||||
|
start = meta.event_start
|
||||||
|
stop = meta.event_stop
|
||||||
|
puts "* event start = #{start}"
|
||||||
|
puts "* event stop = #{stop}"
|
||||||
|
|
||||||
|
if !start.nil? && !stop.nil?
|
||||||
|
duration = Duration.new(start: start, stop: stop)
|
||||||
|
duration_remaining = Duration.new(start: start, stop: stop).from_now
|
||||||
|
puts "* event length = #{duration.to_s} (from now: #{duration_remaining.to_s})"
|
||||||
|
else
|
||||||
|
puts "* event length = (none)"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "* stream url = #{meta.stream_url ? meta.stream_url : "(none)"}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
37
src/duration.cr
Normal file
37
src/duration.cr
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
module FosdemRecorder
|
||||||
|
class Duration
|
||||||
|
getter start : Time
|
||||||
|
getter stop : Time
|
||||||
|
|
||||||
|
def initialize(@start, @stop)
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_now()
|
||||||
|
Duration.new(start: Time.local, stop: self.stop)
|
||||||
|
end
|
||||||
|
|
||||||
|
def value()
|
||||||
|
duration = (self.stop - self.start).to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def past?
|
||||||
|
Time.local > self.stop
|
||||||
|
end
|
||||||
|
|
||||||
|
def future?
|
||||||
|
Time.local < self.start
|
||||||
|
end
|
||||||
|
|
||||||
|
def present?
|
||||||
|
(Time.local >= self.start) && (Time.local <= self.stop)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s()
|
||||||
|
duration = self.value / 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 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
73
src/event_page.cr
Normal file
73
src/event_page.cr
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
|
||||||
|
module FosdemRecorder
|
||||||
|
# Fosdem Cli - Download and cut streams video
|
||||||
|
class EventPage
|
||||||
|
def self.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_sanitized =
|
||||||
|
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?
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
stream_page =
|
||||||
|
page
|
||||||
|
.links
|
||||||
|
.select { |link| link.href =~ /live.fosdem.org/ }
|
||||||
|
.first?
|
||||||
|
.try &.href
|
||||||
|
|
||||||
|
stream_url =
|
||||||
|
stream_page
|
||||||
|
.try &.gsub(%r{.*watch/}, "https://stream.fosdem.org/")
|
||||||
|
.gsub(/$/, ".m3u8")
|
||||||
|
|
||||||
|
|
||||||
|
meta = MetaData.new(
|
||||||
|
title: title,
|
||||||
|
title_sanitized: title_sanitized,
|
||||||
|
stream_url: stream_url,
|
||||||
|
event_start: play_start,
|
||||||
|
event_stop: play_stop,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
src/meta_data.cr
Normal file
19
src/meta_data.cr
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
module FosdemRecorder
|
||||||
|
class MetaData
|
||||||
|
getter title : String?
|
||||||
|
getter title_sanitized : String?
|
||||||
|
getter stream_url : String?
|
||||||
|
|
||||||
|
getter event_start : Time?
|
||||||
|
getter event_stop : Time?
|
||||||
|
|
||||||
|
def initialize(
|
||||||
|
@title,
|
||||||
|
@title_sanitized,
|
||||||
|
@stream_url,
|
||||||
|
@event_start,
|
||||||
|
@event_stop)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
src/utils/url_validator.cr
Normal file
25
src/utils/url_validator.cr
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
|
||||||
|
module FosdemRecorder
|
||||||
|
class UrlValidator
|
||||||
|
class PageIsNotFosdemError < Exception
|
||||||
|
def message
|
||||||
|
"Not a fosdem stream. URL must start with https://fosdem.org/..."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PageIsNotScheduleError < Exception
|
||||||
|
def message
|
||||||
|
"Not a schedule page. URL must contain .../schedule/event/..."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.validate_event!(url) : Nil
|
||||||
|
return if url =~ %r{^https://fosdem.org/\d+/schedule/event/.*}
|
||||||
|
|
||||||
|
raise PageIsNotScheduleError.new if url =~ %r{^https://fosdem.org/.*}
|
||||||
|
raise PageIsNotFosdemError.new
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue