示例#1
0
def _set_id3_opus(converted_file_path: str, song_object: SongObject):
    audio_file = OggOpus(converted_file_path)

    audio_file = _embed_basic_metadata(audio_file, song_object, "opus")
    audio_file = _embed_ogg_metadata(audio_file, song_object)
    audio_file = _embed_cover(audio_file, song_object, "opus")

    audio_file.save()
def _set_id3_opus(converted_file_path, song_obj):
    audio_file = OggOpus(converted_file_path)

    audio_file = _embed_basic_metadata(audio_file, song_obj, "opus")
    audio_file = _embed_ogg_metadata(audio_file, song_obj)
    audio_file = _embed_cover(audio_file, song_obj, "opus")

    audio_file.save()
    def as_opus(self, path, metadata, cached_albumart=None):
        logger.debug('Writing Opus metadata to "{path}".'.format(path=path))
        audiofile = OggOpus(path)

        self._embed_basic_metadata(audiofile, metadata, "opus")
        self._embed_ogg_metadata(audiofile, metadata)
        self._embed_mbp_picture(audiofile, metadata, cached_albumart, "opus")

        audiofile.save()
示例#4
0
 def encodeOpus(self, wavf, dstf, cover, meta):
   # TODO: bitrate 160/128
   FNULL=open(os.devnull, 'w')
   args = ['opusenc', '--bitrate', '192', '--quiet' ]
   if cover:
     args.append('--picture')
     args.append(cover)
   args.append(wavf)
   args.append(dstf)
   subprocess.call(args, stdout=FNULL, stderr=FNULL)
   FNULL.close()
   # tag the file
   opus = OggOpus(dstf)
   # no need to save r128_track_gain
   for c in sorted(meta.keys()):
     opus[c] = meta[c]
   opus.save()
示例#5
0
	def clean(self):
		file = self.cleaned_data.get('file', False)
		acceptedMime = ['audio/mpeg', 'audio/mpeg3', 'audio/x-mpeg-3', 'audio/ogg', 'application/ogg', 'audio/x-ogg', 'application/x-ogg', 'video/ogg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/x-pn-wav']
		type = "none"
		if file:
			if file._size > 10*1024*1024:
				raise forms.ValidationError(file.name + ': File too large. Max Limit: 10MB')
			elif not file.content_type in acceptedMime:
				raise forms.ValidationError(file.name + ': It is not a mp3, ogg, or wav file')

			try:
				OggVorbis(file.temporary_file_path())
				type = 'OGG'
			except:
				pass

			try:
				OggFLAC(file.temporary_file_path())
				type = 'OGG'
			except:
				pass

			try:
				OggOpus(file.temporary_file_path())
				type = 'OGG'
			except:
				pass

			try:
				OggSpeex(file.temporary_file_path())
				type = 'OGG'
			except:
				pass

			try:
				OggTheora(file.temporary_file_path())
				type = 'OGG'
			except:
				pass

			try:
				MP3(file.temporary_file_path())
				type = 'MP3'
			except:
				pass

			try:
				WavPack(file.temporary_file_path())
				type = 'WAV'
			except:
				pass

			if not type in ['OGG', 'MP3', 'WAV']:
				raise forms.ValidationError(file.name + ': Unsupported file type')

			return file
		else:
			raise forms.ValidationError(file.name + ': File cannot be opened')
示例#6
0
    def add_opus(self,
                 path='',
                 filename='file.opus',
                 artist='',
                 album='',
                 title='',
                 tracknum=None,
                 year=None,
                 group='',
                 conductor='',
                 composer='',
                 basefile='silence.opus',
                 apply_tags=True):
        """
        Adds a new ogg opus file with the given parameters to our library.

        Pass in ``False`` for ``apply_tags`` to only use whatever tags happen to
        be present in the source basefile.
        """

        full_filename = self.add_file(basefile, filename, path=path)

        # Finish here if we've been told to.
        if not apply_tags:
            return

        # Apply the tags as specified
        tags = OggOpus(full_filename)
        tags['ARTIST'] = artist
        tags['ALBUM'] = album
        tags['TITLE'] = title

        if group != '':
            tags['ENSEMBLE'] = group
        if conductor != '':
            tags['CONDUCTOR'] = conductor
        if composer != '':
            tags['COMPOSER'] = composer
        if tracknum is not None:
            tags['TRACKNUMBER'] = str(tracknum)
        if year is not None:
            tags['DATE'] = str(year)

        # Save to our filename
        tags.save()
示例#7
0
def write_to_file(music_file):
    if music_file.audioformat == AudioFormat.OGGOPUS:
        audio = OggOpus(music_file.file_path)
        write_to_audio_dict.write(audio, music_file)

    elif music_file.audioformat == AudioFormat.OGGVORBIS:
        audio = OggVorbis(music_file.file_path)
        write_to_audio_dict.write(audio, music_file)

    else:
        logging.warning("Neither ogg vorbis nor opus. Can't write tags")
示例#8
0
 def set_vorbis_data(self, convertedFilePath, songObj):
     # embed song details
     audioFile = OggOpus(convertedFilePath)
     # ! Get rid of all existing ID3 tags (if any exist)
     audioFile.delete()
     # ! song name
     audioFile['title'] = songObj.get_song_name()
     audioFile['titlesort'] = songObj.get_song_name()
     # ! track number
     audioFile['tracknumber'] = str(songObj.get_track_number())
     # ! genres (pretty pointless if you ask me)
     # ! we only apply the first available genre as ID3 v2.3 doesn't support multiple
     # ! genres and ~80% of the world PC's run Windows - an OS with no ID3 v2.4 support
     genres = songObj.get_genres()
     if len(genres) > 0:
         audioFile['genre'] = genres[0]
     # ! all involved artists
     audioFile['artist'] = songObj.get_contributing_artists()
     # ! album name
     audioFile['album'] = songObj.get_album_name()
     # ! album artist (all of 'em)
     audioFile['albumartist'] = songObj.get_album_artists()
     # ! album release date (to what ever precision available)
     audioFile['date'] = songObj.get_album_release()
     audioFile['originaldate'] = songObj.get_album_release()
     audioFile.save()
     # ! setting the album art
     audioFile = OggOpus(convertedFilePath)
     rawAlbumArt = urlopen(songObj.get_album_cover_url()).read()
     AlbumCover = Picture()
     AlbumCover.mime=u'image/jpeg'
     AlbumCover.type=3
     AlbumCover.desc=u'Cover'
     AlbumCover.data=rawAlbumArt
     rawAlbumCover = AlbumCover.write()
     encodedAlbumArt = base64.b64encode(rawAlbumCover)
     vcommentAlbumArt = encodedAlbumArt.decode("ascii")
     audioFile['metadata_block_picture'] = [vcommentAlbumArt]
     audioFile.save()
示例#9
0
def write_cover_opus(afile, cover):
    file_ = OggOpus(afile)

    with open(cover, "rb") as h:
        data = h.read()

    picture = Picture()
    picture.data = data
    picture.type = 17
    picture.desc = u"cover art"
    picture.mime = u"image/" + os.path.splitext(cover)[1][1:]
    dim = [int(x) for x in COVER_DIMENSION_INTERN.split("x")]
    picture.width = dim[0]
    picture.height = dim[1]
    picture.depth = 24

    picture_data = picture.write()
    encoded_data = base64.b64encode(picture_data)
    vcomment_value = encoded_data.decode("ascii")

    file_["metadata_block_picture"] = [vcomment_value]
    file_.save()
示例#10
0
    def test_preserve_non_padding(self):
        self.audio["FOO"] = ["BAR"]
        self.audio.save()

        extra_data = b"\xde\xad\xbe\xef"

        with open(self.filename, "r+b") as fobj:
            OggPage(fobj)  # header
            page = OggPage(fobj)
            data = OggPage.to_packets([page])[0]
            data = data.rstrip(b"\x00") + b"\x01" + extra_data
            new_pages = OggPage.from_packets([data], page.sequence)
            OggPage.replace(fobj, [page], new_pages)

        OggOpus(self.filename).save()

        with open(self.filename, "rb") as fobj:
            OggPage(fobj)  # header
            page = OggPage(fobj)
            data = OggPage.to_packets([page])[0]
            self.assertTrue(data.endswith(b"\x01" + extra_data))

        self.assertEqual(OggOpus(self.filename).tags._padding, 0)
示例#11
0
def isAMusicFile(filePath):
    type = "none"
    try:
        OggVorbis(filePath)
        type = 'OGG'
    except:
        pass

    try:
        OggFLAC(filePath)
        type = 'OGG'
    except:
        pass

    try:
        OggOpus(filePath)
        type = 'OGG'
    except:
        pass

    try:
        OggSpeex(filePath)
        type = 'OGG'
    except:
        pass

    try:
        OggTheora(filePath)
        type = 'OGG'
    except:
        pass

    try:
        MP3(filePath)
        type = 'MP3'
    except:
        pass

    try:
        WavPack(filePath)
        type = 'WAV'
    except:
        pass

    if not type in ['OGG', 'MP3', 'WAV']:
        return False

    return True
示例#12
0
    def test_combine_returns_opus_with_metadata(self, processor,
                                                opus_audio_asset, tmpdir):
        essence = opus_audio_asset.essence
        metadata = dict(ffmetadata=dict(artist='Frédéric Chopin'))

        essence_with_metadata = processor.combine(essence, metadata)

        essence_file = tmpdir.join('essence_with_metadata')
        essence_file.write(essence_with_metadata.read(), 'wb')
        ogg = OggOpus(str(essence_file))
        for key, actual in metadata['ffmetadata'].items():
            expected = ogg.tags.get(key)
            if isinstance(expected, list):
                assert actual in expected
            else:
                assert ogg.tags[key] == actual
示例#13
0
def get_ogg_or_flac(path):
    from mutagen.oggflac import OggFLAC
    from mutagen.oggopus import OggOpus
    from mutagen.oggvorbis import OggVorbis
    from mutagen.flac import FLAC

    try:
        return OggVorbis(path)
    except:
        pass
    try:
        return FLAC(path)
    except:
        pass
    try:
        return OggFLAC(path)
    except:
        pass
    try:
        return OggOpus(path)
    except:
        pass
    return None
示例#14
0
    def get_tags(self):
        file_extension = self.audio_filepath[self.audio_filepath.rindex('.'):]
        if file_extension == '.flac':
            reader = FLAC(self.audio_filepath)
            tags, info = reader.tags, reader.info
            self.title = tags.get('TITLE', ['UNKNOWN'])[0]
            self.artist = tags.get('ARTIST', ['UNKNOWN'])[0]
            self.album = tags.get('ALBUM', ['UNKNOWN'])[0]
            self.duration = float(info.length)

        elif file_extension == '.opus':
            reader = OggOpus(self.audio_filepath)
            tags, info = reader.tags, reader.info
            self.title = tags.get('TITLE', ['UNKNOWN'])[0]
            self.artist = tags.get('ARTIST', ['UNKNOWN'])[0]
            self.album = tags.get('ALBUM', ['UNKNOWN'])[0]
            self.duration = float(info.length)

        elif file_extension == '.mp3':
            reader = MP3(self.audio_filepath)
            tags, info = reader.tags, reader.info
            self.title = tags.get('TIT2', ['UNKNOWN'])[0]
            self.artist = tags.get('TPE1', ['UNKNOWN'])[0]
            self.album = tags.get('TALB', ['UNKNOWN'])[0]
            self.duration = float(info.length)

        elif file_extension == '.m4a':
            reader = MP4(self.audio_filepath)
            tags, info = reader.tags, reader.info
            self.title = tags.get('\xa9nam', ['UNKNOWN'])[0]
            self.artist = tags.get('\xa9ART', ['UNKNOWN'])[0]
            self.album = tags.get('\xa9alb', ['UNKNOWN'])[0]
            self.duration = float(info.length)

        else:
            return
示例#15
0
def _get_opus_tags(track, path):
    """
    Tries to load embedded tags from given file.
    :return: TrackData object
    """
    track_data = TrackData(path)
    log.debug("Importing ogg " + track.path)
    try:
        track.mutagen = OggOpus(path)
    except Exception as e:
        log.warning(
            "Track " + track.path +
            " has no valid tags. Now guessing from file and folder name…")
        return track_data

    track_data.disk = int(__get_common_disk_number(track))
    track_data.length = float(__get_common_track_length(track))
    track_data.cover = __get_ogg_cover(track)
    track_data.author = __get_common_tag(track, "composer")
    track_data.reader = __get_common_tag(track, "artist")
    track_data.book_name = __get_common_tag(track, "album")
    track_data.name = __get_common_tag(track, "title")

    return track_data
示例#16
0
def isSongValid(file):
    acceptedMime = [
        'audio/mp3', 'audio/mpeg', 'audio/mpeg3', 'audio/x-mpeg-3',
        'audio/ogg', 'application/ogg', 'audio/x-ogg', 'application/x-ogg',
        'video/ogg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/x-pn-wav'
    ]
    type = "none"
    if file:
        if file._size > 10 * 1024 * 1024:
            return ('File too large. Max Limit: 10MB')
        elif not file.content_type in acceptedMime:
            if settings.DEBUG:
                return ('MIMETYPE: ' + file.content_type +
                        ' Not a mp3, ogg, or wav file')
            else:
                logger.error('MIMETYPE: ' + file.content_type +
                             ' Not a mp3, ogg, or wav file')
                return ('It is not a mp3, ogg, or wav file')

        try:
            OggVorbis(file.temporary_file_path())
            type = 'OGG'
        except:
            pass

        try:
            OggFLAC(file.temporary_file_path())
            type = 'OGG'
        except:
            pass

        try:
            OggOpus(file.temporary_file_path())
            type = 'OGG'
        except:
            pass

        try:
            OggSpeex(file.temporary_file_path())
            type = 'OGG'
        except:
            pass

        try:
            OggTheora(file.temporary_file_path())
            type = 'OGG'
        except:
            pass

        try:
            MP3(file.temporary_file_path())
            type = 'MP3'
        except:
            pass

        try:
            WavPack(file.temporary_file_path())
            type = 'WAV'
        except:
            pass

        if not type in ['OGG', 'MP3', 'WAV']:
            return ('Unsupported file type')

        return True
    else:
        return ('File cannot be opened')
示例#17
0
def detect_file_codec(path):
    """
    Try to detect codec for file
    """

    if not os.path.isfile(path):
        return None

    name, extension = os.path.splitext(path)

    if extension in ('.m4a', 'caf'):
        from mutagen.mp4 import MP4
        try:
            codec = MP4(path).info.codec
            if codec == 'alac':
                return codec
            elif codec[:4] == 'mp4a':
                return 'aac'
        except Exception as e:
            raise ValueError('Error detecting file {} codec: {}'.format(
                path, e))

    if extension == '.aif':
        from mutagen.aiff import AIFF
        try:
            AIFF(path).info
            return 'aif'
        except Exception as e:
            raise ValueError('Error opening {} as aif: {}'.format(path, e))

    if extension == '.flac':
        from mutagen.flac import FLAC
        try:
            FLAC(path).info
            return 'flac'
        except Exception as e:
            raise ValueError('Error opening {} as flac: {}'.format(path, e))

    if extension == '.mp3':
        from mutagen.mp3 import MP3
        try:
            MP3(path).info
            return 'mp3'
        except Exception as e:
            raise ValueError('Error opening {} as mp3: {}'.format(path, e))

    if extension == '.opus':
        from mutagen.oggopus import OggOpus
        try:
            OggOpus(path).info
            return 'opus'
        except Exception as e:
            raise ValueError('Error opening {} as mp3: {}'.format(path, e))

    if extension == '.ogg':
        from mutagen.oggvorbis import OggVorbis
        try:
            OggVorbis(path).info
            return 'vorbis'
        except Exception as e:
            raise ValueError('Error opening {} as ogg vorbis: {}'.format(
                path, e))

    if extension == '.wv':
        from mutagen.wavpack import WavPack
        try:
            WavPack(path).info
            return 'wavpack'
        except Exception as e:
            raise ValueError('Error opening {} as wavpack: {}'.format(path, e))

    return None
示例#18
0
def getInfoFromTag(filename, language):
    compil = '0'
    tag = None
    if filename.lower().endswith("mp3"):
        try:
            tag = EasyID3(filename)
        except:
            return (None, None, None, None, None, None, None)
        artist = mp3tagGrabber(tag, filename, 'artist', language, force=True)
        album = mp3tagGrabber(tag, filename, 'album', language, force=True)
        title = mp3tagGrabber(tag, filename, 'title', language, force=True)
        track = cleanTrackAndDisk(
            mp3tagGrabber(tag, filename, 'track', language, 'tracknumber'))
        disc = cleanTrackAndDisk(
            mp3tagGrabber(tag, filename, 'disk', language, 'discnumber'))
        TPE2 = mp3tagGrabber(tag, filename, 'TPE2', language, 'performer')
        try:
            compil = tag['compilation'][0]
        except:
            pass
        #print artist, album, title, track, disc, TPE2, compil
        return (artist, album, title, track, disc, TPE2, compil)
    elif filename.lower().endswith("m4a") or filename.lower().endswith(
            "m4b") or filename.lower().endswith("m4p"):
        try:
            tag = EasyMP4(filename)
        except:
            return (None, None, None, None, None, None, None)
    elif filename.lower().endswith("flac"):
        try:
            tag = FLAC(filename)
        except:
            return (None, None, None, None, None, None, None)
    elif filename.lower().endswith("ogg"):
        try:
            tag = OggVorbis(filename)
        except:
            return (None, None, None, None, None, None, None)
    elif filename.lower().endswith("opus"):
        try:
            tag = OggOpus(filename)
        except:
            return (None, None, None, None, None, None, None)
    elif filename.lower().endswith("wma"):
        try:
            tag = ASF(filename)
        except:
            return (None, None, None, None, None, None, None)
        artist = getWMAstring(mutagenGrabber(tag, 'Author', language))
        album = getWMAstring(mutagenGrabber(tag, 'WM/AlbumTitle', language))
        title = getWMAstring(mutagenGrabber(tag, 'Title', language))
        track = cleanTrackAndDisk(
            mutagenGrabber(tag, 'WM/TrackNumber', language))
        disc = cleanTrackAndDisk(mutagenGrabber(tag, 'WM/PartOfSet', language))
        TPE2 = getWMAstring(mutagenGrabber(tag, 'WM/AlbumArtist', language))
        return (artist, album, title, track, disc, TPE2, compil)
    else:  #unsupported filetype
        return (None, None, None, None, None, None, None)
    artist = mutagenGrabber(tag, 'artist', language)
    album = mutagenGrabber(tag, 'album', language)
    title = mutagenGrabber(tag, 'title', language)
    track = cleanTrackAndDisk(mutagenGrabber(tag, 'tracknumber', language))
    disc = cleanTrackAndDisk(mutagenGrabber(tag, 'discnumber', language))
    TPE2 = mutagenGrabber(tag, 'albumartist', language) or mutagenGrabber(
        tag, 'album artist', language)
    try:
        compil = tag['compilation'][0]
        if tag['compilation'][0] == True: compil = '1'
    except:
        pass

    return (artist, album, title, track, disc, TPE2, compil)
示例#19
0
    async def download_song(self, songObj: SongObj) -> None:
        '''
        `songObj` `songObj` : song to be downloaded

        RETURNS `~`

        Downloads, Converts, Normalizes song & embeds metadata as ID3 tags.
        '''

        #! all YouTube downloads are to .\Temp; they are then converted and put into .\ and
        #! finally followed up with ID3 metadata tags

        #! we explicitly use the os.path.join function here to ensure download is
        #! platform agnostic

        # Create a .\Temp folder if not present
        tempFolder = Path('.', 'Temp')

        if not tempFolder.exists():
            tempFolder.mkdir()

        # build file name of converted file
        artistStr = ''

        #! we eliminate contributing artist names that are also in the song name, else we
        #! would end up with things like 'Jetta, Mastubs - I'd love to change the world
        #! (Mastubs REMIX).mp3' which is kinda an odd file name.
        for artist in songObj.get_contributing_artists():
            if artist.lower() not in songObj.get_song_name().lower():
                artistStr += artist + ', '

        #! the ...[:-2] is to avoid the last ', ' appended to artistStr
        convertedFileName = artistStr[:-2] + ' - ' + songObj.get_song_name()

        #! this is windows specific (disallowed chars)
        for disallowedChar in ['/', '?', '\\', '*', '|', '<', '>']:
            if disallowedChar in convertedFileName:
                convertedFileName = convertedFileName.replace(
                    disallowedChar, '')

        #! double quotes (") and semi-colons (:) are also disallowed characters but we would
        #! like to retain their equivalents, so they aren't removed in the prior loop
        convertedFileName = convertedFileName.replace(
            '"', "'").replace(': ', ' - ')

        convertedFilePath = Path(".", f"{convertedFileName}.opus")

        # if a song is already downloaded skip it
        if convertedFilePath.is_file():
            if self.displayManager:
                self.displayManager.notify_download_skip()
            if self.downloadTracker:
                self.downloadTracker.notify_download_completion(songObj)

            #! None is the default return value of all functions, we just explicitly define
            #! it here as a continent way to avoid executing the rest of the function.
            return None

        # download Audio from YouTube
        if self.displayManager:
            youtubeHandler = YouTube(
                url=songObj.get_youtube_link(),
                on_progress_callback=self.displayManager.pytube_progress_hook
            )
        else:
            youtubeHandler = YouTube(songObj.get_youtube_link())

        trackAudioStream = youtubeHandler.streams.filter(
            only_audio=True).order_by('bitrate').last()
        if not trackAudioStream:
            print(f"Unable to get audio stream for \"{songObj.get_song_name()}\" "
                  f"by \"{songObj.get_contributing_artists()[0]}\" "
                  f"from video \"{songObj.get_youtube_link()}\"")
            return None

        downloadedFilePathString = await self._download_from_youtube(convertedFileName, tempFolder,
                                                                     trackAudioStream)

        if downloadedFilePathString is None:
            return None

        downloadedFilePath = Path(downloadedFilePathString)

        # convert downloaded file

        command = 'ffmpeg -v quiet -y -i "%s" -acodec libopus -b:a 80K -vbr on -compression_level 10 "%s"'

        #! bash/ffmpeg on Unix systems need to have excape char (\) for special characters: \$
        #! alternatively the quotes could be reversed (single <-> double) in the command then
        #! the windows special characters needs escaping (^): ^\  ^&  ^|  ^>  ^<  ^^

        if sys.platform == 'win32':
            formattedCommand = command % (
                str(downloadedFilePath),
                str(convertedFilePath)
            )
        else:
            formattedCommand = command % (
                str(downloadedFilePath).replace('$', '\$'),
                str(convertedFilePath).replace('$', '\$')
            )

        process = await asyncio.subprocess.create_subprocess_shell(formattedCommand)
        _ = await process.communicate()

        #! Wait till converted file is actually created
        while True:
            if convertedFilePath.is_file():
                break

        if self.displayManager:
            self.displayManager.notify_conversion_completion()

        # embed song details

        audioFile = OggOpus(convertedFilePath)

        #! Get rid of all existing tags (if any exist)
        audioFile.delete()

        #! song name
        audioFile['title'] = songObj.get_song_name()
        audioFile['titlesort'] = songObj.get_song_name()

        #! track number
        audioFile['tracknumber'] = str(songObj.get_track_number())

        #! genres
        audioFile['genre'] = songObj.get_genres()

        #! all involved artists
        audioFile['artist'] = songObj.get_contributing_artists()

        #! album name
        audioFile['album'] = songObj.get_album_name()

        #! album artist (all of 'em)
        audioFile['albumartist'] = songObj.get_album_artists()

        #! album release date (to what ever precision available)
        audioFile['date'] = songObj.get_album_release()
        audioFile['originaldate'] = songObj.get_album_release()

        audioFile.save()

        #! setting the album art
        audioFile = OggOpus(convertedFilePath)

        rawAlbumArt = urlopen(songObj.get_album_cover_url()).read()

        picture = Picture()
        picture.data = rawAlbumArt
        picture.type = 3
        picture.desc = u"Cover"
        picture.mime = u"image/jpeg"

        picture_data = picture.write()
        encoded_data = base64.b64encode(picture_data)
        vcomment_value = encoded_data.decode("ascii")

        audioFile["metadata_block_picture"] = [vcomment_value]

        audioFile.save()

        # Do the necessary cleanup
        if self.displayManager:
            self.displayManager.notify_download_completion()

        if self.downloadTracker:
            self.downloadTracker.notify_download_completion(songObj)

        # delete the unnecessary YouTube download File
        if downloadedFilePath and downloadedFilePath.is_file():
            downloadedFilePath.unlink()
示例#20
0
    def update_opus(self,
                    filename,
                    artist=None,
                    album=None,
                    title=None,
                    tracknum=None,
                    year=None,
                    group=None,
                    conductor=None,
                    composer=None):
        """
        Updates an on-disk ogg opus file with the given tag data.  Any passed-in
        variable set to None will be ignored.

        If group/conductor/composer is a blank string, those fields will
        be completely removed from the file.  Any of the other fields set
        to blank will leave the tag in place.

        Will ensure that the file's mtime is updated.
        """

        full_filename = self.check_library_filename(filename)
        self.assertEqual(os.path.exists(full_filename), True)

        starting_mtime = int(os.stat(full_filename).st_mtime)

        tags = OggOpus(full_filename)

        if artist is not None:
            try:
                del tags['ARTIST']
            except KeyError:
                pass
            tags['ARTIST'] = artist

        if album is not None:
            try:
                del tags['ALBUM']
            except KeyError:
                pass
            tags['ALBUM'] = album

        if title is not None:
            try:
                del tags['TITLE']
            except KeyError:
                pass
            tags['TITLE'] = title

        if group is not None:
            try:
                del tags['ENSEMBLE']
            except KeyError:
                pass
            if group != '':
                tags['ENSEMBLE'] = group

        if conductor is not None:
            try:
                del tags['CONDUCTOR']
            except KeyError:
                pass
            if conductor != '':
                tags['CONDUCTOR'] = conductor

        if composer is not None:
            try:
                del tags['COMPOSER']
            except KeyError:
                pass
            if composer != '':
                tags['COMPOSER'] = composer

        if tracknum is not None:
            try:
                del tags['TRACKNUMBER']
            except KeyError:
                pass
            tags['TRACKNUMBER'] = str(tracknum)

        if year is not None:
            try:
                del tags['DATE']
            except KeyError:
                pass
            try:
                del tags['YEAR']
            except KeyError:
                pass
            tags['DATE'] = str(year)

        # Save
        tags.save()

        # Check on mtime update and manually fix it if it's not updated
        stat_result = os.stat(full_filename)
        ending_mtime = int(stat_result.st_mtime)
        if starting_mtime == ending_mtime:
            new_mtime = ending_mtime + 1
            os.utime(full_filename, times=(stat_result.st_atime, new_mtime))