Class: MiniradioServer::App

Inherits:
Object
  • Object
show all
Defined in:
lib/miniradio_server/app.rb

Instance Method Summary collapse

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, message|
      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(message || "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.message}"
  # 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.message}"
  @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