├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── download.go ├── event.go ├── go.mod ├── go.sum ├── media.go ├── sync3c.go └── tools └── release.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: muesli 2 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.11.x, 1.12.x, 1.13.x] 8 | platform: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.platform }} 10 | env: 11 | GO111MODULE: "on" 12 | steps: 13 | 14 | - name: Install Go 15 | uses: actions/setup-go@v1 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v1 21 | 22 | - name: Download Go modules 23 | run: go mod download 24 | 25 | - name: Build 26 | run: go build -v ./... 27 | 28 | - name: Test 29 | run: go test ./... 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | sync3c 27 | /dist 28 | /downloads 29 | *~ 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - 3 | binary: sync3c 4 | goos: 5 | - linux 6 | - darwin 7 | - windows 8 | goarch: 9 | - amd64 10 | - arm64 11 | - 386 12 | - arm 13 | goarm: 14 | - 6 15 | - 7 16 | ignore: 17 | - goos: darwin 18 | goarch: 386 19 | - goos: linux 20 | goarch: arm 21 | goarm: 7 22 | sign: 23 | artifacts: checksum 24 | archives: 25 | - 26 | replacements: 27 | darwin: Darwin 28 | linux: Linux 29 | windows: Windows 30 | 386: i386 31 | amd64: x86_64 32 | checksum: 33 | name_template: 'checksums.txt' 34 | snapshot: 35 | name_template: "{{ .Tag }}-next" 36 | changelog: 37 | sort: asc 38 | filters: 39 | exclude: 40 | - '^docs:' 41 | - '^test:' 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sync3c 2 | ====== 3 | 4 | [![Latest Release](https://img.shields.io/github/release/muesli/sync3c.svg)](https://github.com/muesli/sync3c/releases) 5 | [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/muesli/sync3c) 6 | [![Build Status](https://github.com/muesli/sync3c/workflows/build/badge.svg)](https://github.com/muesli/sync3c/actions) 7 | [![Go ReportCard](https://goreportcard.com/badge/muesli/sync3c)](https://goreportcard.com/report/muesli/sync3c) 8 | 9 | A little tool to sync/download media from https://media.ccc.de 10 | 11 | Finds the best available quality of a talk and only downloads that version. It will download each talk in its original language, unless you specify your own preferred language (-language). It will fallback to the original language version if there's no translation available. 12 | 13 | Per default it will download all talks from all conferences. Be careful, this will fetch and store several hundred GiB of videos over the network. It's probably a good idea to pass in the name (-name) of a conference you're interested in. 14 | 15 | If you don't specify a download destination (-destination) everything will be stored in a folder named "downloads" in your current working directory. 16 | 17 | It will create sub-directories for each conference within that destination and will name downloaded files following this schema: "{author} - {title} ({subtitle}) {translation}.ext". If that file already exists, it will skip the download. Hence you should delete the latest started download should you decide to kill the process mid-download. 18 | 19 | ## Installation 20 | 21 | ### Packages & Installers 22 | 23 | - Arch Linux: [sync3c](https://aur.archlinux.org/packages/sync3c/) 24 | - [Binaries](https://github.com/muesli/sync3c/releases) for Linux, macOS & Windows 25 | 26 | ### From Source 27 | 28 | Make sure you have a working Go environment. Follow the [Go install instructions](https://golang.org/doc/install.html). 29 | 30 | To install sync3c, simply run: 31 | 32 | go get github.com/muesli/sync3c 33 | 34 | To compile it from source: 35 | 36 | cd $GOPATH/src/github.com/muesli/sync3c 37 | go get -u -v 38 | go build 39 | 40 | ## Usage 41 | 42 | #### List all available conferences 43 | ``` 44 | $ $GOPATH/bin/sync3c list 45 | Conference: 33c3 (33C3: works for me) 46 | Conference: ds2015 (Datenspuren 2015) 47 | Conference: eh16 (Easterhegg 2016) 48 | Conference: 32c3 (32C3: gated communities) 49 | ... 50 | ``` 51 | 52 | #### Download all talks from all conferences, best available quality & original language only 53 | ``` 54 | $ $GOPATH/bin/sync3c -destination /my/downloads 55 | Conference: 33c3 (33C3: works for me) 56 | Event: Bonsai Kitten waren mir lieber - Rechte Falschmeldungen in sozialen Netzwerken - Auf der Hoaxmap werden seit vergangenem Febru... 57 | Found video (video/mp4): 34 minutes, 1920x1080 (HD: true, 313MiB) https://api.media.ccc.de/public/recordings/13601 58 | Found audio (audio/mpeg): 33 minutes (HD: false, 31MiB) https://api.media.ccc.de/public/recordings/13797 59 | Found audio (audio/opus): 33 minutes (HD: false, 24MiB) https://api.media.ccc.de/public/recordings/13834 60 | Downloading: http://cdn.media.ccc.de/congress/2016/h264-hd/33c3-8288-deu-Bonsai_Kitten_waren_mir_lieber_-_Rechte_Falschmeldungen_in_sozialen_Netzwerken.mp4 61 | Rechte Falschmeldungen-in-sozialen-Netzwerken (...).mp4 313.75 MiB / 313.75 MiB [#################################################] 100.00% 62 | ``` 63 | 64 | #### Download all talks from a specific conference, best available quality & original language only 65 | ``` 66 | $ $GOPATH/bin/sync3c -name eh16 -destination /my/downloads 67 | Conference: eh16 (Easterhegg 2016) 68 | ... 69 | ``` 70 | 71 | #### Download all talks, best available quality for a specific language, with fallback to original language 72 | ``` 73 | $ $GOPATH/bin/sync3c -language eng -destination /my/downloads 74 | ... 75 | ``` 76 | 77 | #### Use a different media source 78 | ``` 79 | $ $GOPATH/bin/sync3c -source media.freifunk.net ... 80 | ... 81 | ``` 82 | 83 | Enjoy the great content! 84 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/kennygrant/sanitize" 11 | "github.com/muesli/goprogressbar" 12 | ) 13 | 14 | // WriteProgressBar is an io.Writer that updates a download progress-bar 15 | type WriteProgressBar struct { 16 | ProgressBar *goprogressbar.ProgressBar 17 | } 18 | 19 | // SizeToString returns a human-readable string for file-sizes 20 | func SizeToString(size uint64) (str string) { 21 | b := float64(size) 22 | 23 | switch { 24 | case size >= 1<<60: 25 | str = fmt.Sprintf("%.2f EiB", b/(1<<60)) 26 | case size >= 1<<50: 27 | str = fmt.Sprintf("%.2f PiB", b/(1<<50)) 28 | case size >= 1<<40: 29 | str = fmt.Sprintf("%.2f TiB", b/(1<<40)) 30 | case size >= 1<<30: 31 | str = fmt.Sprintf("%.2f GiB", b/(1<<30)) 32 | case size >= 1<<20: 33 | str = fmt.Sprintf("%.2f MiB", b/(1<<20)) 34 | case size >= 1<<10: 35 | str = fmt.Sprintf("%.2f KiB", b/(1<<10)) 36 | default: 37 | str = fmt.Sprintf("%dB", size) 38 | } 39 | 40 | return 41 | } 42 | 43 | // Write updates the progress-bar 44 | func (wc *WriteProgressBar) Write(p []byte) (int, error) { 45 | n := len(p) 46 | wc.ProgressBar.Current += int64(n) 47 | wc.ProgressBar.LazyPrint() 48 | 49 | return n, nil 50 | } 51 | 52 | func download(v Conference, e Event, m Recording) error { 53 | author := "" 54 | subtitle := "" 55 | lang := "" 56 | if len(e.Persons) > 0 { 57 | author = sanitize.BaseName(e.Persons[0]) + " - " 58 | } 59 | if len(e.Subtitle) > 0 { 60 | subtitle = " (" + sanitize.BaseName(e.Subtitle) + ")" 61 | } 62 | if e.OriginalLanguage != m.Language { 63 | lang = " [" + m.Language + "]" 64 | } 65 | 66 | path := filepath.Join(downloadPath, sanitize.Path(v.Title)) 67 | basename := fmt.Sprintf("%s%s%s%s", author, sanitize.BaseName(e.Title), subtitle, lang) + "." + extensionForMimeTypes[m.MimeType] 68 | filename := filepath.Join(path, basename) 69 | if _, err := os.Stat(filename); os.IsNotExist(err) { 70 | os.MkdirAll(path, 0755) 71 | 72 | fmt.Println("Downloading:", m.RecordingURL) 73 | out, err := os.Create(filename) 74 | if err != nil { 75 | return err 76 | } 77 | defer out.Close() 78 | 79 | resp, err := http.Get(m.RecordingURL) 80 | if err != nil { 81 | return err 82 | } 83 | defer resp.Body.Close() 84 | 85 | pb := &goprogressbar.ProgressBar{ 86 | Text: filename, 87 | Total: resp.ContentLength, 88 | Width: 60, 89 | PrependTextFunc: func(p *goprogressbar.ProgressBar) string { 90 | return fmt.Sprintf("%s / %s", 91 | SizeToString(uint64(p.Current)), 92 | SizeToString(uint64(p.Total))) 93 | }, 94 | } 95 | 96 | src := io.TeeReader(resp.Body, &WriteProgressBar{ProgressBar: pb}) 97 | _, err = io.Copy(out, src) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | fmt.Println() 103 | } else { 104 | fmt.Println("File", filename, "already exists - skipping!") 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // Event represents a single event 9 | type Event struct { 10 | ConferenceURL string `json:"conference_url"` 11 | Date string `json:"date"` 12 | Description string `json:"description"` 13 | FrontendLink string `json:"frontend_link"` 14 | GUID string `json:"guid"` 15 | Length int64 `json:"length"` 16 | Link string `json:"link"` 17 | OriginalLanguage string `json:"original_language"` 18 | Persons []string `json:"persons"` 19 | PosterURL string `json:"poster_url"` 20 | ReleaseDate string `json:"release_date"` 21 | Slug string `json:"slug"` 22 | Subtitle string `json:"subtitle"` 23 | Tags []string `json:"tags"` 24 | ThumbURL string `json:"thumb_url"` 25 | Title string `json:"title"` 26 | UpdatedAt string `json:"updated_at"` 27 | URL string `json:"url"` 28 | } 29 | 30 | // Events represents a series of events 31 | type Events struct { 32 | Acronym string `json:"acronym"` 33 | AspectRatio string `json:"aspect_ratio"` 34 | Events []Event `json:"events"` 35 | ImagesURL string `json:"images_url"` 36 | LogoURL string `json:"logo_url"` 37 | RecordingsURL string `json:"recordings_url"` 38 | ScheduleURL string `json:"schedule_url"` 39 | Slug string `json:"slug"` 40 | Title string `json:"title"` 41 | UpdatedAt string `json:"updated_at"` 42 | URL string `json:"url"` 43 | WebgenLocation string `json:"webgen_location"` 44 | } 45 | 46 | func findEvents(url string) (Events, error) { 47 | event := Events{} 48 | 49 | r, err := http.Get(url) 50 | if err != nil { 51 | return event, err 52 | } 53 | defer r.Body.Close() 54 | 55 | err = json.NewDecoder(r.Body).Decode(&event) 56 | return event, err 57 | } 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muesli/sync3c 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/kennygrant/sanitize v1.2.4 7 | github.com/muesli/goprogressbar v0.0.0-20190807022807-e540249d2ac1 8 | github.com/olekukonko/tablewriter v0.0.4 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= 2 | github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 3 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 4 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 5 | github.com/muesli/goprogressbar v0.0.0-20190807022807-e540249d2ac1 h1:NROB7UaQ4VVE0mDQKHWhkmwL3YLXLEcmDbpLB99oc8Y= 6 | github.com/muesli/goprogressbar v0.0.0-20190807022807-e540249d2ac1/go.mod h1:19yRWZtJozyS7m+fyTUK0rE76LABdnU7zp0BuyeDwLc= 7 | github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= 8 | github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 11 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 12 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 13 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 15 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 16 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 18 | -------------------------------------------------------------------------------- /media.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // Recording represents a single recording 9 | type Recording struct { 10 | ConferenceURL string `json:"conference_url"` 11 | EventURL string `json:"event_url"` 12 | Filename string `json:"filename"` 13 | Folder string `json:"folder"` 14 | Height int64 `json:"height"` 15 | HighQuality bool `json:"high_quality"` 16 | Language string `json:"language"` 17 | Length int64 `json:"length"` 18 | MimeType string `json:"mime_type"` 19 | RecordingURL string `json:"recording_url"` 20 | Size int64 `json:"size"` 21 | State string `json:"state"` 22 | UpdatedAt string `json:"updated_at"` 23 | URL string `json:"url"` 24 | Width int64 `json:"width"` 25 | } 26 | 27 | // Media represents a single talk 28 | type Media struct { 29 | ConferenceURL string `json:"conference_url"` 30 | Date string `json:"date"` 31 | Description string `json:"description"` 32 | FrontendLink string `json:"frontend_link"` 33 | GUID string `json:"guid"` 34 | Length int64 `json:"length"` 35 | Link string `json:"link"` 36 | OriginalLanguage string `json:"original_language"` 37 | Persons []string `json:"persons"` 38 | PosterURL string `json:"poster_url"` 39 | Recordings []Recording `json:"recordings"` 40 | ReleaseDate string `json:"release_date"` 41 | Slug string `json:"slug"` 42 | Subtitle string `json:"subtitle"` 43 | Tags []string `json:"tags"` 44 | ThumbURL string `json:"thumb_url"` 45 | Title string `json:"title"` 46 | UpdatedAt string `json:"updated_at"` 47 | URL string `json:"url"` 48 | } 49 | 50 | func findMedia(url string) (Media, error) { 51 | media := Media{} 52 | 53 | r, err := http.Get(url) 54 | if err != nil { 55 | return media, err 56 | } 57 | defer r.Body.Close() 58 | 59 | err = json.NewDecoder(r.Body).Decode(&media) 60 | return media, err 61 | } 62 | -------------------------------------------------------------------------------- /sync3c.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/kennygrant/sanitize" 13 | "github.com/olekukonko/tablewriter" 14 | ) 15 | 16 | var ( 17 | preferredMimeTypes = []string{"video/webm", "video/mp4", "video/ogg", "audio/ogg", "audio/opus", "audio/mpeg", "application/x-subrip"} 18 | extensionForMimeTypes = make(map[string]string) 19 | 20 | downloadPath string 21 | name string 22 | language string 23 | source string 24 | ) 25 | 26 | // Conference represents a single conference 27 | type Conference struct { 28 | Acronym string `json:"acronym"` 29 | AspectRatio string `json:"aspect_ratio"` 30 | ImagesURL string `json:"images_url"` 31 | LogoURL string `json:"logo_url"` 32 | RecordingsURL string `json:"recordings_url"` 33 | ScheduleURL string `json:"schedule_url"` 34 | Slug string `json:"slug"` 35 | Title string `json:"title"` 36 | UpdatedAt string `json:"updated_at"` 37 | URL string `json:"url"` 38 | WebgenLocation string `json:"webgen_location"` 39 | } 40 | 41 | // Conferences is a list of conferences 42 | type Conferences struct { 43 | Conferences []Conference `json:"conferences"` 44 | } 45 | 46 | // ByTitle implements sort.Interface based on the Title field. 47 | type ByTitle []Conference 48 | 49 | func (a ByTitle) Len() int { return len(a) } 50 | func (a ByTitle) Less(i, j int) bool { return a[i].Title < a[j].Title } 51 | func (a ByTitle) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 52 | 53 | func priorityForMimeType(mime string) int { 54 | for i, v := range preferredMimeTypes { 55 | if strings.ToLower(mime) == v { 56 | return i 57 | } 58 | } 59 | 60 | return -1 61 | } 62 | 63 | func findConferences(url string) (Conferences, error) { 64 | ci := Conferences{} 65 | 66 | r, err := http.Get(url) 67 | if err != nil { 68 | return ci, err 69 | } 70 | defer r.Body.Close() 71 | 72 | err = json.NewDecoder(r.Body).Decode(&ci) 73 | return ci, err 74 | } 75 | 76 | func listConferences() { 77 | ci, err := findConferences(fmt.Sprintf("https://api.%s/public/conferences", source)) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | sort.Sort(ByTitle(ci.Conferences)) 83 | 84 | table := tablewriter.NewWriter(os.Stdout) 85 | table.SetHeader([]string{"Conference", "Title"}) 86 | table.SetBorders(tablewriter.Border{Left: false, Top: false, Right: false, Bottom: false}) 87 | table.SetAlignment(tablewriter.ALIGN_LEFT) 88 | table.SetAutoWrapText(false) 89 | 90 | for _, v := range ci.Conferences { 91 | table.Append([]string{v.Acronym, v.Title}) 92 | } 93 | table.Render() 94 | } 95 | 96 | func main() { 97 | flag.StringVar(&name, "name", "", "download media of a specific conference only (e.g. '33c3')") 98 | flag.StringVar(&downloadPath, "destination", "./downloads/", "where to store downloaded media") 99 | flag.StringVar(&language, "language", "", "preferred language if available (eng, deu or fra)") 100 | flag.StringVar(&source, "source", "media.ccc.de", "source of conferences") 101 | flag.Parse() 102 | 103 | var listOnly bool 104 | if len(flag.Args()) > 0 { 105 | arg := strings.ToLower(flag.Args()[0]) 106 | listOnly = arg == "list" 107 | } 108 | 109 | if listOnly { 110 | listConferences() 111 | return 112 | } 113 | 114 | name = strings.ToLower(name) 115 | language = strings.ToLower(language) 116 | source = strings.ToLower(source) 117 | 118 | extensionForMimeTypes["video/webm"] = "webm" 119 | extensionForMimeTypes["video/mp4"] = "mp4" 120 | extensionForMimeTypes["video/ogg"] = "ogm" 121 | extensionForMimeTypes["audio/ogg"] = "ogg" 122 | extensionForMimeTypes["audio/opus"] = "opus" 123 | extensionForMimeTypes["audio/mpeg"] = "mp3" 124 | 125 | ci, err := findConferences(fmt.Sprintf("https://api.%s/public/conferences", source)) 126 | if err != nil { 127 | panic(err) 128 | } 129 | 130 | found := false 131 | for _, v := range ci.Conferences { 132 | if len(name) > 0 && name != strings.ToLower(v.Acronym) { 133 | continue 134 | } 135 | fmt.Printf("Conference: %s (%s)\n", v.Acronym, v.Title) 136 | found = true 137 | 138 | events, err := findEvents(v.URL) 139 | if err != nil { 140 | panic(err) 141 | } 142 | 143 | for _, e := range events.Events { 144 | desc := strings.Replace(sanitize.HTML(e.Description), "\n", "", -1) 145 | if len(desc) > 48 { 146 | desc = desc[:45] + "..." 147 | } 148 | if len(desc) > 0 { 149 | desc = " - " + desc 150 | } 151 | fmt.Printf("Event: %s%s\n", e.Title, desc) 152 | 153 | media, err := findMedia(e.URL) 154 | if err != nil { 155 | panic(err) 156 | } 157 | 158 | bestMatch := Recording{} 159 | highestPriority := -1 160 | if len(media.Recordings) == 0 { 161 | panic("No recordings found for this event!") 162 | } 163 | for _, m := range media.Recordings { 164 | if (len(language) == 0 || language != strings.ToLower(m.Language)) && 165 | m.Language != e.OriginalLanguage { 166 | continue 167 | } 168 | 169 | if m.Width == 0 { 170 | fmt.Printf("\tFound other/audio (%s): %d minutes (HD: %t, %dMiB) %s\n", m.MimeType, m.Length/60, m.HighQuality, m.Size, m.URL) 171 | } else { 172 | fmt.Printf("\tFound video (%s): %d minutes, %dx%d (HD: %t, %dMiB) %s\n", m.MimeType, m.Length/60, m.Width, m.Height, m.HighQuality, m.Size, m.URL) 173 | } 174 | 175 | prio := priorityForMimeType(m.MimeType) 176 | if prio < 0 { 177 | fmt.Println("Unknown mimetype encountered:" + m.MimeType) 178 | } 179 | 180 | pick := false 181 | if highestPriority == -1 { 182 | // if we got nothing so far, always pick any available option 183 | pick = true 184 | } 185 | if strings.ToLower(bestMatch.Language) != language && strings.ToLower(m.Language) == language { 186 | // we already found something, but this is the preferred language 187 | pick = true 188 | } else { 189 | if prio < highestPriority || (prio == highestPriority && m.Width > bestMatch.Width) { 190 | // we already found something, but this has a higher resolution 191 | pick = true 192 | } 193 | } 194 | 195 | if pick { 196 | highestPriority = prio 197 | bestMatch = m 198 | } 199 | } 200 | 201 | if len(bestMatch.RecordingURL) == 0 { 202 | fmt.Println("Could not find any desired version of this event, sorry. Skipping!") 203 | continue 204 | } 205 | err = download(v, e, bestMatch) 206 | if err != nil { 207 | panic(err) 208 | } 209 | 210 | fmt.Println() 211 | } 212 | } 213 | 214 | if found { 215 | fmt.Println("Done.") 216 | } else { 217 | fmt.Println("Couldn't find any conference with acronym", name) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tools/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! command -v goreleaser; then 4 | echo "goreleaser not found" 5 | exit 1 6 | fi 7 | 8 | # Get the highest tag number 9 | VERSION="$(git describe --abbrev=0 --tags)" 10 | VERSION=${VERSION:-'0.0.0'} 11 | 12 | # Get number parts 13 | MAJOR="${VERSION%%.*}"; VERSION="${VERSION#*.}" 14 | MINOR="${VERSION%%.*}"; VERSION="${VERSION#*.}" 15 | PATCH="${VERSION%%.*}"; VERSION="${VERSION#*.}" 16 | 17 | # Increase version 18 | PATCH=$((PATCH+1)) 19 | 20 | TAG="${1}" 21 | 22 | if [ "${TAG}" = "" ]; then 23 | TAG="${MAJOR}.${MINOR}.${PATCH}" 24 | fi 25 | 26 | echo "Releasing ${TAG} ..." 27 | 28 | git tag -a -s -m "Release ${TAG}" "${TAG}" 29 | git push --tags 30 | goreleaser release --rm-dist 31 | --------------------------------------------------------------------------------