class EL_YOUTUBE_VIDEO

(source code)

description

Youtube video

note
	description: "Youtube video"

	author: "Finnian Reilly"
	copyright: "Copyright (c) 2001-2022 Finnian Reilly"
	contact: "finnian at eiffel hyphen loop dot com"

	license: "MIT license (See: en.wikipedia.org/wiki/MIT_License)"
	date: "2023-08-17 15:52:36 GMT (Thursday 17th August 2023)"
	revision: "20"

class
	EL_YOUTUBE_VIDEO

inherit
	ANY

	EL_MODULE_FILE_SYSTEM; EL_MODULE_DIRECTORY; EL_MODULE_LIO; EL_MODULE_USER_INPUT

	EL_ZSTRING_CONSTANTS; EL_STRING_8_CONSTANTS

	EL_YOUTUBE_CONSTANTS

create
	make, make_default

feature {NONE} -- Initialization

	make (a_url: ZSTRING; a_output_dir: DIR_PATH)
		require
			not_empty: not a_url.is_empty
		do
			make_default
			url := a_url; output_dir := a_output_dir

			lio.put_labeled_string ("Fetching formats for", a_url)
			lio.put_new_line_x2

			if attached Cmd_get_youtube_options as cmd then
				cmd.put_string (Var.url, a_url)
				cmd.execute

				audio_stream_list.fill (cmd.lines)
				video_stream_list.fill (cmd.lines)
			end
		end

	make_default
		do
			create url.make_empty
			create title.make_empty
			create output_dir
			create output_path
			create download_list.make (2)
			create audio_stream_list.make (Maximum_stream_count)
			create video_stream_list.make (Maximum_stream_count)
		end

feature -- Access

	get_output_extension: STRING
		local
			option_list: LIST [STRING]; menu_select: EL_USER_MENU_SELECT
		do
			Result := Empty_string_8
			inspect download_list.count
				when 1 then
					if attached download_list.audio as audio then
						Result := audio.stream.extension
					end

				when 2 then
					if attached download_list.video as video then
						option_list := video.stream.extension_set.to_list
						create menu_select.make ("Select container type", option_list)
						menu_select.select_index
						if not option_list.off then
							Result := option_list.item
						end
					end
			else
			end
		end

	title: ZSTRING

	url: ZSTRING

feature -- Status query

	downloads_selected: BOOLEAN
		do
			Result := download_list.count > 0
		end

	has_streams: BOOLEAN
		do
			Result := audio_stream_list.count + video_stream_list.count > 0
		end

	is_merge_complete: BOOLEAN
		-- `True' if `output_path.exists'
		do
			Result := output_path.exists
		end

feature -- Element change

	set_title (a_title: like title)
		do
			if a_title.is_empty then
				title := default_title
			else
				title := a_title
			end
			across download_list as download loop
				download.item.set_file_path (title, output_dir)
			end
		end

feature -- Basic operations

	download_all (output_extension: STRING)
		locaL
			done: BOOLEAN; audio_path: FILE_PATH
			video_extension: STRING
		do
			from until done loop
				download_list.download_all
				if download_list.exist then
					if attached download_list.video as video_download then
						video_extension := video_download.stream.extension
						if video_extension ~ output_extension then
							merge_streams
						elseif video_extension ~ MP4_extension then
							convert_streams_to_mp4
						end
						if is_merge_complete then
							cleanup
							done := True
						else
							done := not User_input.approved_action_y_n ("Merging of streams failed. Retry?")
						end

					elseif attached download_list.audio as audio_download then
--						audio only
						audio_path := audio_download.base_output_path
						audio_path.add_extension (audio_download.stream.extension)
						File_system.rename_file (audio_download.file_path, audio_path)
						done := True
					end
				else
					done := not User_input.approved_action_y_n ("Download of streams failed. Retry?")
				end
			end
		end

	select_downloads
		require
			has_streams: has_streams
		local
			quit: BOOLEAN
		do
			download_list.wipe_out
			across stream_list_array as array until quit loop
				if attached array.item as list then
					list.get_user_choice (url, download_list)
					if list.selected_index = 0 then
						download_list.wipe_out
						quit := True
					end
				end
			end
		end

feature {NONE} -- Implementation

	cleanup
		do
			if output_path.exists then
				across download_list as download loop
					download.item.remove
				end
			end
		end

	convert_streams_to_mp4
		require
			valid_downloads: downloads_selected
		do
			if attached download_list.video as video then
				output_path := video.base_output_path
				output_path.add_extension (MP4_extension)
				do_conversion (Cmd_convert_to_mp4, "Converting to")
				lio.put_new_line
			end
		end

	default_title: ZSTRING
		local
			index: INTEGER
		do
			if attached Cmd_get_youtube_file_name as cmd then
				lio.put_labeled_string ("Getting title for", url)
				lio.put_new_line
				lio.put_character ('.')
				cmd.put_string (Var.url, url)
				cmd.execute
				lio.put_new_line
				if cmd.lines.count > 0 then
					Result := cmd.lines.first
					across << '-', '.' >> as c until index > 0 loop
						index := Result.last_index_of (c.item, Result.count)
					end
					if index > 0 then
						Result.keep_head (index - 1)
					end
					lio.put_labeled_string ("Title", Result)
					lio.put_new_line_x2
				else
					Result := "untitled"
				end
			end
		end

	do_conversion (command: EL_OS_COMMAND; description: STRING)
		local
			socket: EL_UNIX_STREAM_SOCKET; progress_display: EL_CONSOLE_PROGRESS_DISPLAY
			pos_out_time: INTEGER; total_duration: DOUBLE; line: STRING; time: TIME
		do
			total_duration := video_duration_fine_seconds
			lio.put_labeled_string (description, output_path.to_string)
			lio.put_new_line

			across download_list as download loop
				download.item.set_command_path (command)
			end

			command.put_path (Var.output_path, output_path)
			command.put_path (Var.socket_path, Socket_path)
			command.put_string (Var.title, title)

			create socket.make_server (Socket_path)
			socket.listen (1)
			socket.set_blocking

			command.set_forking_mode (True)
			command.execute

			socket.accept
			-- Track progress via Unix socket
			if attached {EL_STREAM_SOCKET} socket.accepted as ffmpeg_socket then
				create progress_display.make
				from until ffmpeg_socket.was_error loop
					ffmpeg_socket.read_line
					if ffmpeg_socket.was_error then
						line := ffmpeg_socket.last_string (False)
						pos_out_time := line.substring_index (Out_time_field, 1)
						if pos_out_time > 0 then
							time := new_time (line.substring (pos_out_time + Out_time_field.count, line.count))
							progress_display.set_progress (time.duration.fine_seconds_count / total_duration)
						end
					end
				end
				ffmpeg_socket.close
			end
			socket.close
		end

	merge_streams
		-- merge audio and video streams using same video container
		require
			valid_downloads: downloads_selected
		do
			if attached download_list.video as video then
				output_path := video.base_output_path
				output_path.add_extension (video.file_path.extension)
				do_conversion (Cmd_merge, "Merging audio and video streams to")
				lio.put_new_line
			end
		end

	new_time (time_string: STRING): TIME
		do
			create Result.make_from_string (time_string, once "[0]hh:[0]mi:[0]ss.ff3")
		end

	stream_list_array: ARRAY [EL_YOUTUBE_STREAM_LIST]
		do
			Result := << audio_stream_list, video_stream_list >>
		end

	video_duration_fine_seconds: DOUBLE
		do
			if attached download_list.video as download then
				Cmd_video_duration.put_path (Var.video_path, download.file_path)
				Cmd_video_duration.execute
				if attached Cmd_video_duration.lines.first as first then
					Result := new_time (first.substring_between_general ("Duration: ", ", ", 1)).fine_seconds
				end
			end
		end

feature {NONE} -- Internal attributes

	audio_stream_list: EL_YOUTUBE_AUDIO_STREAM_LIST

	download_list: EL_YOUTUBE_STREAM_DOWNLOAD_LIST

	output_dir: DIR_PATH

	output_path: FILE_PATH

	video_stream_list: EL_YOUTUBE_VIDEO_STREAM_LIST

feature {NONE} -- Constants

	Maximum_stream_count: INTEGER = 10

	Out_time_field: STRING = "out_time="

	Socket_path: FILE_PATH
		once
			Result := Directory.temporary + "el_toolkit-youtube_dl.sock"
		end

end