package main import ( "fmt" "io" "log" "net/http" "os" "strings" "time" "github.com/kkdai/youtube/v2" ) const ( ContentTypeAudioM4A = "audio/m4a" ItagM4AAudio = 140 DefaultHTTPBindAddr = "0.0.0.0:8888" DefaultTimeout = 5 * time.Second ) type AudioDownloader struct { videoID string ytClient youtube.Client reader io.Reader } func NewAudioDownloader(videoID string) (*AudioDownloader, error) { var ( reader io.Reader ytClient youtube.Client ) cachePath := fmt.Sprintf("cache/%s.m4a", videoID) fetch := true if _, err := os.Stat(cachePath); err == nil { if fptr, err := os.Open(cachePath); err == nil { reader = fptr fetch = false } else { log.Printf("warning: error opening cache file: %v", err) } } if fetch { log.Println("fetching audio stream from youtube...") video, err := ytClient.GetVideo(videoID) if err != nil { return nil, fmt.Errorf("error fetching video: %v", err) } format := video.Formats.FindByItag(ItagM4AAudio) log.Printf("M4A format expected to contain %d bytes", format.ContentLength) stream, _, err := ytClient.GetStream(video, format) if err != nil { return nil, fmt.Errorf("error fetching stream: %v", err) } // TODO: only allow the cached file to be accessed after it has been // successfully downloaded. cacheFile, err := os.Create(cachePath) if err != nil { return nil, fmt.Errorf("error creating cache file: %v", err) } reader = io.TeeReader(stream, cacheFile) } return &AudioDownloader{ videoID: videoID, ytClient: ytClient, reader: reader, }, nil } func (d *AudioDownloader) Read(p []byte) (int, error) { return d.reader.Read(p) } func (d *AudioDownloader) Close() error { if rc, ok := d.reader.(io.ReadCloser); ok { return rc.Close() } return nil } func handleRequest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("method not allowed")) return } if !strings.HasPrefix(r.URL.Path, "/api/audio") { w.WriteHeader(http.StatusNotFound) w.Write([]byte("page not found")) return } videoID := r.URL.Query().Get("video_id") if videoID == "" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("no video ID provided")) return } downloader, err := NewAudioDownloader(videoID) if err != nil { log.Printf("downloader error: %v", err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("could not download video")) return } defer downloader.Close() w.Header().Add("Content-Type", ContentTypeAudioM4A) w.Header().Add("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) n, err := io.Copy(w, downloader) if err != nil { log.Printf("error writing response: %v", err) return } log.Printf("wrote %d bytes for video ID %s", n, videoID) } func main() { srv := http.Server{ ReadTimeout: DefaultTimeout, WriteTimeout: DefaultTimeout, Addr: DefaultHTTPBindAddr, Handler: http.HandlerFunc(handleRequest), } log.Fatal(srv.ListenAndServe()) }