diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 97d245d..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/YouTubeMDBot/__init__.py b/YouTubeMDBot/__init__.py index ea5b0d9..4f6844f 100755 --- a/YouTubeMDBot/__init__.py +++ b/YouTubeMDBot/__init__.py @@ -27,6 +27,7 @@ from .audio import FFmpegMP3 from .audio import FFmpegOGG from .audio import FFmpegOpener +from .audio import FFmpegM4A from .audio import FPCalc from .audio import ffmpeg_available @@ -41,6 +42,7 @@ from .downloader import YouTubeDownloader from .downloader import MultipleYouTubeDownloader +from .downloader import M4AYouTubeDownloader from .metadata import AudioMetadata from .metadata import MetadataIdentifier diff --git a/YouTubeMDBot/audio/__init__.py b/YouTubeMDBot/audio/__init__.py index 49eab31..e822578 100755 --- a/YouTubeMDBot/audio/__init__.py +++ b/YouTubeMDBot/audio/__init__.py @@ -16,5 +16,6 @@ from ..audio.ffmpeg import FFmpegOpener from ..audio.ffmpeg import FFmpegOGG from ..audio.ffmpeg import FFmpegMP3 +from ..audio.ffmpeg import FFmpegM4A from ..audio.ffmpeg import ffmpeg_available from ..audio.fpcalc import FPCalc diff --git a/YouTubeMDBot/audio/ffmpeg.py b/YouTubeMDBot/audio/ffmpeg.py index f94c5ae..03ef697 100755 --- a/YouTubeMDBot/audio/ffmpeg.py +++ b/YouTubeMDBot/audio/ffmpeg.py @@ -21,6 +21,7 @@ from ..constants import FFMPEG_OPENER from ..constants import FFMPEG_CONVERTER +from ..constants import FFMPEG_VOLUME def ffmpeg_available() -> bool: @@ -99,6 +100,16 @@ def get_extra(self) -> bytes: """ return self.__err + def get_volume(self) -> float: + """ + Gets the maximum volume of the data input. + :return: the volume. + """ + command = FFMPEG_VOLUME + proc = Popen(command, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=True) + out, err = proc.communicate(self._data) + return float(out.decode("utf-8")) if out.decode("utf-8") != '' else 0.0 + class FFmpegOpener(FFmpeg): """ @@ -169,3 +180,26 @@ def convert(self) -> int: command.append("ogg") command.append("-") return self.process() + + +class FFmpegM4A(FFmpegExporter): + def __init__(self, data: bytes, filename: str, bitrate: str = None): + super().__init__(data, bitrate) + self.filename = filename + + def convert(self) -> int: + command = super().get_command() + vol = self.get_volume() * -1 + command.append("-af") + command.append(f"volume={vol}dB") + if self._bitrate: + command.append("-b:a") + command.append(self._bitrate) + command.append("-c:a") + command.append("aac") + command.append("-movflags") + command.append("faststart") + command.append("-f") + command.append("ipod") + command.append(self.filename) + return self.process() diff --git a/YouTubeMDBot/constants/__init__.py b/YouTubeMDBot/constants/__init__.py index 961f325..3375f6d 100755 --- a/YouTubeMDBot/constants/__init__.py +++ b/YouTubeMDBot/constants/__init__.py @@ -20,6 +20,7 @@ from ..constants.app_constants import PROGRAM_ARGS from ..constants.app_constants import FFMPEG_OPENER from ..constants.app_constants import FFMPEG_CONVERTER +from ..constants.app_constants import FFMPEG_VOLUME from ..constants.app_constants import DB_HOST from ..constants.app_constants import DB_NAME from ..constants.app_constants import DB_PASSWORD diff --git a/YouTubeMDBot/constants/app_constants.py b/YouTubeMDBot/constants/app_constants.py index 383f34c..2fec883 100755 --- a/YouTubeMDBot/constants/app_constants.py +++ b/YouTubeMDBot/constants/app_constants.py @@ -20,7 +20,8 @@ PROGRAM_ARGS = sys.argv # YouTube DL options -YDL_CLI_OPTIONS = ["youtube-dl", "--format", "bestaudio[ext=m4a]", "--quiet", "--output", +YDL_CLI_OPTIONS = ["youtube-dl", "--format", "bestaudio[ext=m4a]", "--quiet", + "--output", "-"] # FPCalc command @@ -41,8 +42,10 @@ # FFmpeg commands FFMPEG_OPENER = "ffmpeg -i - -f s16le -".split(" ") -FFMPEG_CONVERTER = ["ffmpeg", "-i", "-", "-vn", "-map_metadata", "0", - "-movflags", "use_metadata_tags"] +FFMPEG_CONVERTER = ["ffmpeg", "-y", "-i", "-", "-vn", "-map_metadata", "0", + "-map", "a:a", "-movflags", "use_metadata_tags"] +FFMPEG_VOLUME = "ffmpeg -i - -af \"volumedetect\" -f null /dev/null 2>&1 | " \ + "grep \"max_volume\" | awk -F': ' '{print $2}' | cut -d ' ' -f1" MAX_PROCESS = cpu_count() diff --git a/YouTubeMDBot/downloader/__init__.py b/YouTubeMDBot/downloader/__init__.py index abec918..315ac3f 100755 --- a/YouTubeMDBot/downloader/__init__.py +++ b/YouTubeMDBot/downloader/__init__.py @@ -15,3 +15,4 @@ # along with this program. If not, see . from ..downloader.youtube_downloader import MultipleYouTubeDownloader from ..downloader.youtube_downloader import YouTubeDownloader +from ..downloader.youtube_downloader import M4AYouTubeDownloader diff --git a/YouTubeMDBot/downloader/youtube_downloader.py b/YouTubeMDBot/downloader/youtube_downloader.py index af6d51b..b94df2d 100755 --- a/YouTubeMDBot/downloader/youtube_downloader.py +++ b/YouTubeMDBot/downloader/youtube_downloader.py @@ -17,8 +17,10 @@ from typing import Any from typing import Callable from typing import Tuple +from tempfile import NamedTemporaryFile from .. import ThreadPoolBase +from .. import FFmpegM4A from ..constants.app_constants import YDL_CLI_OPTIONS @@ -63,6 +65,27 @@ def get_url(self) -> str: return self.__url +class M4AYouTubeDownloader(YouTubeDownloader): + def __init__(self, url: str, bitrate: str = None): + super().__init__(url) + self.user_bitrate = bitrate + + def download(self) -> Tuple[BytesIO, bytes]: + io, data = super().download() + m4a_file = NamedTemporaryFile(suffix=".m4a") + m4a_converter = FFmpegM4A(data=data, + filename=m4a_file.name, + bitrate=self.user_bitrate) + ret = m4a_converter.convert() + if ret != 0: + raise RuntimeError("ffmpeg is unable to convert file - output: " + + m4a_converter.get_extra().decode("utf-8")) + with open(m4a_file.name, "rb") as out_m4a: + m4a_data = out_m4a.read() + m4a_file.close() + return BytesIO(m4a_data), m4a_data + + class MultipleYouTubeDownloader(ThreadPoolBase): def __new__(cls, max_processes: int = 4, diff --git a/YouTubeMDBot/tests/m4adownloader.py b/YouTubeMDBot/tests/m4adownloader.py new file mode 100644 index 0000000..0c5b6f7 --- /dev/null +++ b/YouTubeMDBot/tests/m4adownloader.py @@ -0,0 +1,27 @@ +import unittest +import mutagen + +from YouTubeMDBot.downloader import M4AYouTubeDownloader +from YouTubeMDBot.audio import FFmpegM4A + + +class MyTestCase(unittest.TestCase): + def test_download(self): + dl = M4AYouTubeDownloader( + url="https://www.youtube.com/watch?v=s6VaeFCxta8", + bitrate="128k") + io, data = dl.download() + with open("outex.m4a", "wb") as of: + of.write(data) + print(mutagen.File(io).pprint()) + io.seek(0) + return io, data + + def test_normalization(self): + io, data = self.test_download() + ctr = FFmpegM4A(data, "filename") + assert ctr.get_volume() == 0.0 + + +if __name__ == '__main__': + unittest.main()