Class: MiniradioServer::App
- Inherits:
-
Object
- Object
- MiniradioServer::App
- Defined in:
- lib/miniradio_server/app.rb
Instance Method Summary collapse
- #call(env) ⇒ Object
- #get_mp3_list ⇒ Object
- #index ⇒ Object
-
#initialize(mp3_dir, cache_dir, ffmpeg_cmd, segment_duration, logger) ⇒ App
constructor
A new instance of App.
Constructor Details
#initialize(mp3_dir, cache_dir, ffmpeg_cmd, segment_duration, logger) ⇒ App
Returns a new instance of App.
15 16 17 18 19 20 21 22 23 |
# File 'lib/miniradio_server/app.rb', line 15 def initialize(mp3_dir, cache_dir, ffmpeg_cmd, segment_duration, logger) @mp3_dir = Pathname.new(mp3_dir).realpath @cache_dir = Pathname.new(cache_dir).realpath @ffmpeg_cmd = ffmpeg_cmd @segment_duration = segment_duration @logger = logger # For managing locks during conversion processing (using Mutex per file) @conversion_locks = Hash.new { |h, k| h[k] = Mutex.new } # Mutex is built-in, no require needed end |
Instance Method Details
#call(env) ⇒ Object
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/miniradio_server/app.rb', line 25 def call(env) request_path = env['PATH_INFO'] @logger.info "Request received: #{request_path}" # Root URL match = request_path.match(%r{^/$|^/index(\.html)$}) if match return response(200, index, 'text/html') end # Path pattern: /stream/{mp3_basename}/{playlist or segment} # mp3_basename is the filename without the extension match = request_path.match(%r{^/stream/([^/]+)/(.+\.(m3u8|mp3))$}) unless match @logger.warn "Invalid request path format: #{request_path}" return not_found_response("Not Found (Invalid Path Format)") end mp3_basename = URI.decode_uri_component(match[1]) # e.g., "your_music" (without extension) requested_filename = match[2] # e.g., "playlist.m3u8" or "segment001.mp3" extension = match[3].downcase # "m3u8" or "mp3" # --- Check if the original MP3 file exists --- # Security: Check for directory traversal in basename if mp3_basename.include?('..') || mp3_basename.include?('/') @logger.warn "Invalid MP3 base name requested: #{mp3_basename}" return forbidden_response("Invalid filename.") end original_mp3_path = @mp3_dir.join("#{mp3_basename}.mp3") unless original_mp3_path.exist? && original_mp3_path.file? @logger.warn "Original MP3 file not found: #{original_mp3_path}" return not_found_response("Not Found (Original MP3)") end # --- Build cache paths --- cache_subdir = @cache_dir.join(mp3_basename) hls_playlist_path = cache_subdir.join("playlist.m3u8") requested_cache_file_path = cache_subdir.join(requested_filename) # Security: Check if the requested cache file path is within the cache subdirectory # Use string comparison as realpath fails if the file doesn't exist yet unless requested_cache_file_path.to_s.start_with?(cache_subdir.to_s + File::SEPARATOR) || requested_cache_file_path == hls_playlist_path @logger.warn "Attempted access outside cache directory: #{requested_cache_file_path}" return forbidden_response("Access denied.") end # --- Process based on request type --- if extension == 'm3u8' # M3U8 request: Check if conversion is needed, convert if necessary, and serve ensure_hls_converted(original_mp3_path, cache_subdir, hls_playlist_path) do |status, | case status when :ok, :already_exists return serve_file(hls_playlist_path) when :converting # Another process/thread is converting return service_unavailable_response("Conversion in progress. Please try again shortly.") when :error return internal_server_error_response( || "HLS conversion failed.") end end elsif extension == 'mp3' # MP3 segment request: Serve from cache (404 if not found) # Normally, the m3u8 is requested first, so the cache should exist if requested_cache_file_path.exist? && requested_cache_file_path.file? return serve_file(requested_cache_file_path) else # Segment request might come before m3u8, or an invalid request after conversion failure @logger.warn "Segment file not found (cache not generated or invalid request?): #{requested_cache_file_path}" # For simplicity, return 404. A more robust check might verify parent conversion status. return not_found_response("Not Found (Segment)") end else # Should not reach here @logger.error "Unexpected file extension: #{extension}" return internal_server_error_response end rescue SystemCallError => e # File access related errors (ENOENT, EACCES, etc.) @logger.error "File access error: #{e.}" # Return 404 or 500 depending on the context return not_found_response("Resource not found or access denied") rescue => e @logger.error "Unexpected error occurred: #{e.}" @logger.error e.backtrace.join("\n") return internal_server_error_response end |
#get_mp3_list ⇒ Object
114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/miniradio_server/app.rb', line 114 def get_mp3_list r = [] @mp3_dir.glob("*.mp3").each do |file| mp3 = {} Mp3Info.open(file) do |mp3info| mp3[:title] = mp3info.tag.title mp3[:artist] = mp3info.tag.artist mp3[:album] = mp3info.tag.album mp3[:file] = file.basename(".mp3") end r << mp3 end r end |
#index ⇒ Object
129 130 131 132 |
# File 'lib/miniradio_server/app.rb', line 129 def index template = Tilt::SlimTemplate.new("#{__dir__}/templ/index.html.slim") template.render(self, :mp3_list => get_mp3_list) end |