Experimental tag support redesign

This redesign completely changes how tags are stored in memory. Now all
arbitrary tag names are supported, where possible. Some extra work will
be needed to support arbitrary tags with TagLib, such as replacing it
with a different library.

Translation pending for a couple of strings.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
xcode15
Christopher Snowhill 2022-07-08 06:26:28 -07:00
parent 44e1fc5c49
commit 8ee4a04f3b
27 changed files with 868 additions and 627 deletions

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21179.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21179.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -15,16 +15,16 @@
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Info Inspector" separatorStyle="none" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" visibleAtLaunch="NO" animationBehavior="utilityWindow" frameAutosaveName="InfoInspector" titlebarAppearsTransparent="YES" id="1" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<rect key="contentRect" x="700" y="80" width="300" height="582"/>
<rect key="contentRect" x="700" y="80" width="300" height="604"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<value key="minSize" type="size" width="240" height="550"/>
<value key="minSize" type="size" width="240" height="572"/>
<value key="maxSize" type="size" width="400" height="600"/>
<view key="contentView" id="2">
<rect key="frame" x="0.0" y="0.0" width="300" height="582"/>
<rect key="frame" x="0.0" y="0.0" width="300" height="604"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9">
<rect key="frame" x="-2" y="526" width="107" height="14"/>
<rect key="frame" x="-2" y="548" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Artist:" id="10">
<font key="font" metaFont="smallSystem"/>
@ -33,7 +33,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="11">
<rect key="frame" x="-2" y="504" width="107" height="14"/>
<rect key="frame" x="-2" y="526" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Album:" id="12">
<font key="font" metaFont="smallSystem"/>
@ -42,7 +42,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="13">
<rect key="frame" x="-2" y="460" width="107" height="14"/>
<rect key="frame" x="-2" y="482" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Track:" id="14">
<font key="font" metaFont="smallSystem"/>
@ -51,7 +51,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="15">
<rect key="frame" x="-2" y="438" width="107" height="14"/>
<rect key="frame" x="-2" y="460" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Length:" id="16">
<font key="font" metaFont="smallSystem"/>
@ -60,16 +60,16 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="17">
<rect key="frame" x="-2" y="416" width="107" height="14"/>
<rect key="frame" x="-2" y="438" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Year:" id="18">
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Date:" id="18">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="19">
<rect key="frame" x="-2" y="394" width="107" height="14"/>
<rect key="frame" x="-2" y="416" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Genre:" id="20">
<font key="font" metaFont="smallSystem"/>
@ -78,7 +78,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="21">
<rect key="frame" x="-2" y="350" width="107" height="14"/>
<rect key="frame" x="-2" y="372" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Sample Rate:" id="22">
<font key="font" metaFont="smallSystem"/>
@ -87,7 +87,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="27">
<rect key="frame" x="-2" y="328" width="107" height="14"/>
<rect key="frame" x="-2" y="350" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Channels:" id="28">
<font key="font" metaFont="smallSystem"/>
@ -96,7 +96,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="29">
<rect key="frame" x="-2" y="306" width="107" height="14"/>
<rect key="frame" x="-2" y="328" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Bitrate:" id="32">
<font key="font" metaFont="smallSystem"/>
@ -105,7 +105,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="30">
<rect key="frame" x="-2" y="284" width="107" height="14"/>
<rect key="frame" x="-2" y="306" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Bits Per Sample:" id="31">
<font key="font" metaFont="smallSystem"/>
@ -114,7 +114,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="23">
<rect key="frame" x="-2" y="482" width="107" height="14"/>
<rect key="frame" x="-2" y="504" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Title:" id="24">
<font key="font" metaFont="smallSystem"/>
@ -123,7 +123,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="33" customClass="ToolTipTextField">
<rect key="frame" x="113" y="526" width="170" height="14"/>
<rect key="frame" x="113" y="548" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="34">
<font key="font" metaFont="smallSystem"/>
@ -135,7 +135,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="35" customClass="ToolTipTextField">
<rect key="frame" x="113" y="504" width="170" height="14"/>
<rect key="frame" x="113" y="526" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="36">
<font key="font" metaFont="smallSystem"/>
@ -147,7 +147,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="37" customClass="ToolTipTextField">
<rect key="frame" x="113" y="482" width="170" height="14"/>
<rect key="frame" x="113" y="504" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="38">
<font key="font" metaFont="smallSystem"/>
@ -159,7 +159,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="39" customClass="ToolTipTextField">
<rect key="frame" x="113" y="460" width="170" height="14"/>
<rect key="frame" x="113" y="482" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="40">
<font key="font" metaFont="smallSystem"/>
@ -171,7 +171,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="41" customClass="ToolTipTextField">
<rect key="frame" x="113" y="438" width="170" height="14"/>
<rect key="frame" x="113" y="460" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="42">
<font key="font" metaFont="smallSystem"/>
@ -183,7 +183,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="43" customClass="ToolTipTextField">
<rect key="frame" x="113" y="416" width="170" height="14"/>
<rect key="frame" x="113" y="438" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="44">
<font key="font" metaFont="smallSystem"/>
@ -191,11 +191,11 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="valueToDisplay.yearText" id="miZ-gp-CqU"/>
<binding destination="-2" name="value" keyPath="valueToDisplay.date" id="EUq-4E-Lz3"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="45" customClass="ToolTipTextField">
<rect key="frame" x="113" y="394" width="170" height="14"/>
<rect key="frame" x="113" y="416" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="46">
<font key="font" metaFont="smallSystem"/>
@ -207,7 +207,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="49" customClass="ToolTipTextField">
<rect key="frame" x="113" y="350" width="170" height="14"/>
<rect key="frame" x="113" y="372" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="50">
<font key="font" metaFont="smallSystem"/>
@ -223,7 +223,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="51" customClass="ToolTipTextField">
<rect key="frame" x="113" y="328" width="170" height="14"/>
<rect key="frame" x="113" y="350" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="52">
<font key="font" metaFont="smallSystem"/>
@ -235,7 +235,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="53" customClass="ToolTipTextField">
<rect key="frame" x="113" y="306" width="170" height="14"/>
<rect key="frame" x="113" y="328" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="54">
<font key="font" metaFont="smallSystem"/>
@ -247,7 +247,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="55" customClass="ToolTipTextField">
<rect key="frame" x="113" y="284" width="170" height="14"/>
<rect key="frame" x="113" y="306" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="56">
<font key="font" metaFont="smallSystem"/>
@ -259,7 +259,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QPg-Mb-Urn">
<rect key="frame" x="-2" y="262" width="107" height="14"/>
<rect key="frame" x="-2" y="284" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Codec:" id="cbq-TT-CZX">
<font key="font" metaFont="smallSystem"/>
@ -268,7 +268,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ijS-y2-eCZ" customClass="ToolTipTextField">
<rect key="frame" x="113" y="262" width="170" height="14"/>
<rect key="frame" x="113" y="284" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="Yby-OU-cqP">
<font key="font" metaFont="smallSystem"/>
@ -280,7 +280,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bti-s6-SIU">
<rect key="frame" x="-2" y="240" width="107" height="14"/>
<rect key="frame" x="-2" y="262" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Encoding:" id="8e7-lp-K5l">
<font key="font" metaFont="smallSystem"/>
@ -289,7 +289,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L4f-rE-CN3" customClass="ToolTipTextField">
<rect key="frame" x="113" y="240" width="170" height="14"/>
<rect key="frame" x="113" y="262" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="v14-AG-B9D">
<font key="font" metaFont="smallSystem"/>
@ -301,7 +301,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gn9-9b-eV8">
<rect key="frame" x="-2" y="218" width="107" height="14"/>
<rect key="frame" x="-2" y="240" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Cuesheet:" id="Cu5-ia-Z5b">
<font key="font" metaFont="smallSystem"/>
@ -310,7 +310,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WOl-SC-4tu" customClass="ToolTipTextField">
<rect key="frame" x="113" y="218" width="170" height="14"/>
<rect key="frame" x="113" y="240" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="UyE-Mc-e39">
<font key="font" metaFont="smallSystem"/>
@ -322,7 +322,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="arA-Pj-ANg">
<rect key="frame" x="-2" y="196" width="107" height="14"/>
<rect key="frame" x="-2" y="218" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="ReplayGain:" id="qBi-M8-kEx">
<font key="font" metaFont="smallSystem"/>
@ -331,7 +331,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2xx-It-i6I" customClass="ToolTipTextField">
<rect key="frame" x="113" y="196" width="170" height="14"/>
<rect key="frame" x="113" y="218" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="rAo-AP-lVa">
<font key="font" metaFont="smallSystem"/>
@ -344,7 +344,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="84">
<rect key="frame" x="-2" y="372" width="107" height="14"/>
<rect key="frame" x="-2" y="394" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Filename:" id="85">
<font key="font" metaFont="smallSystem"/>
@ -353,7 +353,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="86" customClass="ToolTipTextField">
<rect key="frame" x="113" y="372" width="170" height="14"/>
<rect key="frame" x="113" y="394" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="87">
<font key="font" metaFont="smallSystem"/>
@ -365,7 +365,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vB6-9J-5qg">
<rect key="frame" x="-2" y="548" width="107" height="14"/>
<rect key="frame" x="-2" y="570" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Album Artist:" id="LFJ-QQ-gGr">
<font key="font" metaFont="smallSystem"/>
@ -374,7 +374,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cj0-Tw-xpq" customClass="ToolTipTextField">
<rect key="frame" x="113" y="548" width="170" height="14"/>
<rect key="frame" x="113" y="570" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="B8w-o8-ZBw">
<font key="font" metaFont="smallSystem"/>
@ -386,7 +386,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FWx-Sc-Ocv">
<rect key="frame" x="-2" y="174" width="107" height="14"/>
<rect key="frame" x="-2" y="196" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Play Count:" id="fiv-eh-w3c">
<font key="font" metaFont="smallSystem"/>
@ -394,8 +394,17 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cd3-Qt-hCm">
<rect key="frame" x="-2" y="174" width="107" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Comment:" id="Ule-N3-dKW">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WSs-wC-mWc" customClass="ToolTipTextField">
<rect key="frame" x="113" y="174" width="170" height="14"/>
<rect key="frame" x="113" y="196" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="Ial-XI-91y">
<font key="font" metaFont="smallSystem"/>
@ -407,6 +416,19 @@
<binding destination="-2" name="toolTip" keyPath="valueToDisplay.playCountInfo" id="ydF-ec-fBX"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ef3-yG-qT1" customClass="ToolTipTextField">
<rect key="frame" x="113" y="174" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" selectable="YES" sendsActionOnEndEditing="YES" title="N/A" id="PPV-dt-9Bp">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="toolTip" keyPath="valueToDisplay.comment" id="XzQ-nd-jMU"/>
<binding destination="-2" name="value" keyPath="valueToDisplay.comment" id="Esa-PB-mpv"/>
</connections>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="RWn-fb-0wT">
<rect key="frame" x="0.0" y="20" width="300" height="146"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES"/>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21223.12" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AlbumArtwork" representedClassName="AlbumArtwork" syncable="YES" codeGenerationType="class">
<attribute name="artData" optional="YES" attributeType="Binary"/>
<attribute name="artHash" optional="YES" attributeType="String"/>
@ -15,10 +15,7 @@
<attribute name="title" optional="YES" attributeType="String"/>
</entity>
<entity name="PlaylistEntry" representedClassName="PlaylistEntry" syncable="YES" codeGenerationType="class">
<attribute name="album" optional="YES" attributeType="String"/>
<attribute name="albumartist" optional="YES" attributeType="String"/>
<attribute name="artHash" optional="YES" attributeType="String"/>
<attribute name="artist" optional="YES" attributeType="String"/>
<attribute name="bitrate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="bitsPerSample" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelConfig" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@ -30,19 +27,18 @@
<attribute name="currentPosition" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="dbIndex" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="deLeted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="disc" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="encoding" optional="YES" attributeType="String"/>
<attribute name="endian" optional="YES" attributeType="String"/>
<attribute name="entryId" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="error" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="errorMessage" optional="YES" attributeType="String"/>
<attribute name="floatingPoint" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="genre" optional="YES" attributeType="String"/>
<attribute name="index" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="metadataCompressed" optional="YES" attributeType="Binary"/>
<attribute name="metadataDecompressedSize" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="metadataLoaded" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="queued" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="queuePosition" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rawTitle" optional="YES" attributeType="String"/>
<attribute name="removed" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="replayGainAlbumGain" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="replayGainAlbumPeak" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
@ -55,12 +51,10 @@
<attribute name="spotlightTrack" optional="YES" attributeType="String"/>
<attribute name="stopAfter" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="totalFrames" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="track" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="trashUrlString" optional="YES" attributeType="String"/>
<attribute name="unSigned" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="urlString" optional="YES" attributeType="String"/>
<attribute name="volume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="YES"/>
<attribute name="year" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<entity name="SandboxToken" representedClassName="SandboxToken" syncable="YES" codeGenerationType="class">
<attribute name="bookmark" optional="YES" attributeType="Binary"/>
@ -70,7 +64,7 @@
<elements>
<element name="AlbumArtwork" positionX="0" positionY="207" width="128" height="59"/>
<element name="PlayCount" positionX="-18" positionY="171" width="128" height="149"/>
<element name="PlaylistEntry" positionX="-36" positionY="9" width="128" height="719"/>
<element name="PlaylistEntry" positionX="-36" positionY="9" width="128" height="629"/>
<element name="SandboxToken" positionX="-18" positionY="171" width="128" height="74"/>
</elements>
</model>

View File

@ -74,6 +74,25 @@
@property(nonatomic, readonly) float rating;
@property(nonatomic) NSString *_Nullable album;
@property(nonatomic) NSString *_Nullable albumartist;
@property(nonatomic) NSString *_Nullable artist;
@property(nonatomic) NSString *_Nullable rawTitle;
@property(nonatomic) NSString *_Nullable genre;
@property(nonatomic) int32_t disc;
@property(nonatomic) int32_t track;
@property(nonatomic) int32_t year;
@property(nonatomic) NSString *_Nullable date;
@property(nonatomic) NSString *_Nullable comment;
@property(nonatomic) NSDictionary *_Nullable metadataBlob;
- (NSString *_Nullable)readAllValuesAsString:(NSString *_Nonnull)tagName;
- (void)setValue:(NSString *_Nonnull)tagName fromString:(NSString *_Nullable)value;
- (void)addValue:(NSString *_Nonnull)tagName fromString:(NSString *_Nonnull)value;
- (void)setMetadata:(NSDictionary *_Nonnull)metadata;
@end

View File

@ -16,11 +16,28 @@
#import "SHA256Digest.h"
#import "SecondsFormatter.h"
#import <compression.h>
extern NSPersistentContainer *kPersistentContainer;
extern NSMutableDictionary<NSString *, AlbumArtwork *> *kArtworkDictionary;
static NSMutableDictionary *kMetadataBlobCache = nil;
static NSMutableDictionary *kMetadataCache = nil;
static void *kCompressionScratchBuffer = NULL;
static void *kDecompressionScratchBuffer = NULL;
@implementation PlaylistEntry (Extension)
+ (void)initialize {
if(!kMetadataBlobCache) {
kMetadataBlobCache = [[NSMutableDictionary alloc] init];
}
if(!kMetadataCache) {
kMetadataCache = [[NSMutableDictionary alloc] init];
}
}
// The following read-only keys depend on the values of other properties
+ (NSSet *)keyPathsForValuesAffectingUrl {
@ -503,8 +520,72 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path) {
self.error = YES;
self.errorMessage = NSLocalizedStringFromTableInBundle(@"ErrorMetadata", nil, [NSBundle bundleForClass:[self class]], @"");
} else {
NSMutableDictionary *metaDict = [[NSMutableDictionary alloc] init];
self.volume = 1;
[self setValuesForKeysWithDictionary:metadata];
for(NSString *key in metadata) {
NSString *lowerKey = [key lowercaseString];
id valueObj = [metadata objectForKey:key];
NSArray *values = nil;
NSString *firstValue = nil;
NSData *dataValue = nil;
if([valueObj isKindOfClass:[NSArray class]]) {
values = (NSArray *)valueObj;
if([values count]) {
firstValue = values[0];
}
} else if([valueObj isKindOfClass:[NSString class]]) {
firstValue = (NSString *)valueObj;
values = @[firstValue];
} else if([valueObj isKindOfClass:[NSNumber class]]) {
NSNumber *numberValue = (NSNumber *)valueObj;
firstValue = [numberValue stringValue];
values = @[firstValue];
} else if([valueObj isKindOfClass:[NSData class]]) {
dataValue = (NSData *)valueObj;
}
if([lowerKey isEqualToString:@"bitrate"]) {
self.bitrate = [firstValue intValue];
} else if([lowerKey isEqualToString:@"bitspersample"]) {
self.bitsPerSample = [firstValue intValue];
} else if([lowerKey isEqualToString:@"channelconfig"]) {
self.channelConfig = [firstValue intValue];
} else if([lowerKey isEqualToString:@"channels"]) {
self.channels = [firstValue intValue];
} else if([lowerKey isEqualToString:@"codec"]) {
self.codec = firstValue;
} else if([lowerKey isEqualToString:@"cuesheet"]) {
self.cuesheet = firstValue;
} else if([lowerKey isEqualToString:@"encoding"]) {
self.encoding = firstValue;
} else if([lowerKey isEqualToString:@"endian"]) {
self.endian = firstValue;
} else if([lowerKey isEqualToString:@"floatingpoint"]) {
self.floatingPoint = [firstValue boolValue];
} else if([lowerKey isEqualToString:@"samplerate"]) {
self.sampleRate = [firstValue floatValue];
} else if([lowerKey isEqualToString:@"seekable"]) {
self.seekable = [firstValue boolValue];
} else if([lowerKey isEqualToString:@"totalframes"]) {
self.totalFrames = [firstValue integerValue];
} else if([lowerKey isEqualToString:@"unsigned"]) {
self.unSigned = [firstValue boolValue];
} else if([lowerKey isEqualToString:@"replaygain_album_gain"]) {
self.replayGainAlbumGain = [firstValue floatValue];
} else if([lowerKey isEqualToString:@"replaygain_album_peak"]) {
self.replayGainAlbumPeak = [firstValue floatValue];
} else if([lowerKey isEqualToString:@"replaygain_track_gain"]) {
self.replayGainTrackGain = [firstValue floatValue];
} else if([lowerKey isEqualToString:@"replaygain_track_peak"]) {
self.replayGainTrackPeak = [firstValue floatValue];
} else if([lowerKey isEqualToString:@"volume"]) {
self.volume = [firstValue floatValue];
} else if([lowerKey isEqualToString:@"albumart"]) {
self.albumArt = dataValue;
} else {
[metaDict setObject:values forKey:lowerKey];
}
}
self.metadataBlob = metaDict;
}
[self setMetadataLoaded:YES];
@ -574,4 +655,318 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path) {
}
}
@dynamic album;
- (NSString *)album {
return [self readAllValuesAsString:@"album"];
}
- (void)setAlbum:(NSString *)album {
[self setValue:@"album" fromString:album];
}
@dynamic albumartist;
- (NSString *)albumartist {
NSString *value = [self readAllValuesAsString:@"albumartist"];
if(!value) {
value = [self readAllValuesAsString:@"album artist"];
}
if(!value) {
value = [self readAllValuesAsString:@"album_artist"];
}
return value;
}
- (void)setAlbumartist:(NSString *)albumartist {
[self setValue:@"albumartist" fromString:albumartist];
[self setValue:@"album artist" fromString:nil];
[self setValue:@"album_artist" fromString:nil];
}
@dynamic artist;
- (NSString *)artist {
return [self readAllValuesAsString:@"artist"];
}
- (void)setArtist:(NSString *)artist {
[self setValue:@"artist" fromString:artist];
}
@dynamic rawTitle;
- (NSString *)rawTitle {
return [self readAllValuesAsString:@"title"];
}
- (void)setRawTitle:(NSString *)rawTitle {
[self setValue:@"title" fromString:rawTitle];
}
@dynamic genre;
- (NSString *)genre {
return [self readAllValuesAsString:@"genre"];
}
- (void)setGenre:(NSString *)genre {
[self setValue:@"genre" fromString:genre];
}
@dynamic disc;
- (int32_t)disc {
NSString *value = [self readAllValuesAsString:@"discnumber"];
if(!value) {
value = [self readAllValuesAsString:@"discnum"];
}
if(!value) {
value = [self readAllValuesAsString:@"disc"];
}
if(value) {
return [value intValue];
} else {
return 0;
}
}
- (void)setDisc:(int32_t)disc {
[self setValue:@"discnumber" fromString:[NSString stringWithFormat:@"%u", disc]];
[self setValue:@"discnum" fromString:nil];
[self setValue:@"disc" fromString:nil];
}
@dynamic track;
- (int32_t)track {
NSString *value = [self readAllValuesAsString:@"tracknumber"];
if(!value) {
value = [self readAllValuesAsString:@"tracknum"];
}
if(!value) {
value = [self readAllValuesAsString:@"track"];
}
if(value) {
return [value intValue];
} else {
return 0;
}
}
@dynamic year;
- (int32_t)year {
NSString *value = [self readAllValuesAsString:@"date"];
if(!value) {
value = [self readAllValuesAsString:@"recording_date"];
}
if(!value) {
value = [self readAllValuesAsString:@"year"];
}
if(value) {
return [value intValue];
} else {
return 0;
}
}
- (void)setYear:(int32_t)year {
NSString *svalue = [NSString stringWithFormat:@"%u", year];
[self setValue:@"year" fromString:svalue];
[self setValue:@"date" fromString:nil];
[self setValue:@"recording_date" fromString:nil];
}
@dynamic date;
- (NSString *)date {
NSString *value = [self readAllValuesAsString:@"date"];
if(!value) {
value = [self readAllValuesAsString:@"recording_date"];
}
if(!value) {
value = [self readAllValuesAsString:@"year"];
}
return value;
}
- (void)setDate:(NSString *)date {
[self setValue:@"date" fromString:date];
[self setValue:@"recording_date" fromString:nil];
[self setValue:@"year" fromString:nil];
}
@dynamic comment;
- (NSString *)comment {
return [self readAllValuesAsString:@"comment"];
}
- (void)setComment:(NSString *)comment {
[self setValue:@"comment" fromString:comment];
}
- (NSString *_Nullable)readAllValuesAsString:(NSString *_Nonnull)tagName {
NSMutableDictionary *dict = [kMetadataCache objectForKey:self.urlString];
if(dict) {
NSString *value = [dict objectForKey:tagName];
if(value) {
return value;
}
}
id metaObj = self.metadataBlob;
if(metaObj && [metaObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *metaDict = (NSDictionary *)metaObj;
NSArray *values = [metaDict objectForKey:tagName];
if(values) {
NSString *value = [values componentsJoinedByString:@", "];
if(!dict) {
dict = [[NSMutableDictionary alloc] init];
[kMetadataCache setObject:dict forKey:self.urlString];
}
[dict setObject:value forKey:tagName];
return value;
}
}
return nil;
}
- (void)deleteAllValues {
self.metadataBlob = nil;
[kMetadataCache removeObjectForKey:self.urlString];
}
- (void)deleteValue:(NSString *_Nonnull)tagName {
id metaObj = self.metadataBlob;
if(metaObj && [metaObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *metaDict = (NSDictionary *)metaObj;
NSMutableDictionary *metaDictCopy = [metaDict mutableCopy];
[metaDictCopy removeObjectForKey:tagName];
self.metadataBlob = [NSDictionary dictionaryWithDictionary:metaDictCopy];
}
}
- (void)setValue:(NSString *_Nonnull)tagName fromString:(NSString *_Nullable)value {
if(!value) {
[self deleteValue:tagName];
return;
}
NSArray *values = [value componentsSeparatedByString:@", "];
id metaObj = self.metadataBlob;
if(metaObj && [metaObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *metaDict = (NSDictionary *)metaObj;
NSMutableDictionary *metaDictCopy = [metaDict mutableCopy];
[metaDictCopy setObject:values forKey:tagName];
self.metadataBlob = [NSDictionary dictionaryWithDictionary:metaDictCopy];
}
}
- (void)addValue:(NSString *_Nonnull)tagName fromString:(NSString *_Nonnull)value {
NSMutableDictionary *dict = [kMetadataCache objectForKey:self.urlString];
if(dict) {
[dict removeObjectForKey:tagName];
}
id metaObj = self.metadataBlob;
if(metaObj && [metaObj isKindOfClass:[NSDictionary class]]) {
NSDictionary *metaDict = (NSDictionary *)metaObj;
NSMutableDictionary *metaDictCopy = [metaDict mutableCopy];
NSArray *values = [metaDictCopy objectForKey:tagName];
NSMutableArray *valuesCopy;
if(values) {
valuesCopy = [values mutableCopy];
} else {
valuesCopy = [[NSMutableArray alloc] init];
}
[valuesCopy addObject:value];
values = [NSArray arrayWithArray:valuesCopy];
[metaDictCopy setObject:values forKey:tagName];
self.metadataBlob = [NSDictionary dictionaryWithDictionary:metaDictCopy];
}
}
- (NSDictionary *)metadataBlob {
{
NSDictionary *blob = [kMetadataBlobCache objectForKey:self.urlString];
if(blob) {
return blob;
}
}
if(self.metadataCompressed == nil || self.metadataDecompressedSize == 0) {
return @{};
}
if(!kDecompressionScratchBuffer) {
size_t scratchSize = compression_decode_scratch_buffer_size(COMPRESSION_ZLIB);
kDecompressionScratchBuffer = malloc(scratchSize);
}
void *decompressionBuffer = malloc(self.metadataDecompressedSize);
size_t decodedBytes = compression_decode_buffer(decompressionBuffer, self.metadataDecompressedSize, [self.metadataCompressed bytes], [self.metadataCompressed length], kDecompressionScratchBuffer, COMPRESSION_ZLIB);
NSData *decodedData = [NSData dataWithBytes:decompressionBuffer length:decodedBytes];
free(decompressionBuffer);
NSSet *allowed = [NSSet setWithArray:@[[NSDictionary class], [NSMutableDictionary class], [NSArray class], [NSMutableArray class], [NSString class]]];
NSError *error = nil;
NSDictionary *dict;
dict = [NSKeyedUnarchiver unarchivedObjectOfClasses:allowed
fromData:decodedData
error:&error];
if(!dict) {
dict = @{};
}
[kMetadataBlobCache setObject:dict forKey:self.urlString];
return dict;
}
- (void)setMetadataBlob:(NSMutableDictionary *)metadataBlob {
[kMetadataCache removeObjectForKey:self.urlString];
if(metadataBlob == nil) {
self.metadataCompressed = nil;
self.metadataDecompressedSize = 0;
[kMetadataBlobCache removeObjectForKey:self.urlString];
return;
}
[kMetadataBlobCache setObject:metadataBlob forKey:self.urlString];
NSError *error = nil;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:metadataBlob
requiringSecureCoding:YES
error:&error];
if(!kCompressionScratchBuffer) {
size_t scratchBufferSize = compression_encode_scratch_buffer_size(COMPRESSION_ZLIB);
kCompressionScratchBuffer = malloc(scratchBufferSize);
}
size_t fullSize = [data length];
size_t compressedSize = fullSize * 3 / 2 + 256;
void *compressedBuffer = malloc(compressedSize);
size_t compressedOutSize = compression_encode_buffer(compressedBuffer, compressedSize, [data bytes], fullSize, kCompressionScratchBuffer, COMPRESSION_ZLIB);
NSData *compressData = [NSData dataWithBytes:compressedBuffer length:compressedOutSize];
free(compressedBuffer);
self.metadataCompressed = compressData;
self.metadataDecompressedSize = fullSize;
}
@end

View File

@ -861,6 +861,8 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path);
}
- (BOOL)addDataStore {
BOOL dataMigrated = [[NSUserDefaults standardUserDefaults] boolForKey:@"metadataMigrated"];
NSPersistentContainer *pc = playlistController.persistentContainer;
if(pc) {
NSManagedObjectContext *moc = pc.viewContext;
@ -902,10 +904,17 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path);
++index;
}
[resultsCopy removeObjectsAtIndexes:pruneSet];
if([pruneSet count]) {
if(!dataMigrated) {
for(PlaylistEntry *pe in resultsCopy) {
pe.metadataLoaded = NO;
}
}
if([pruneSet count] || !dataMigrated) {
[playlistController commitPersistentStore];
}
results = [NSArray arrayWithArray:resultsCopy];
{
@ -917,6 +926,8 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path);
[playlistController readQueueFromDataStore];
[playlistController readShuffleListFromDataStore];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"metadataMigrated"];
return YES;
}
return NO;

View File

@ -85,10 +85,10 @@
if([[track track] intValue]) [cuesheetMetadata setValue:@([[track track] intValue]) forKey:@"track"];
if([track genre]) [cuesheetMetadata setValue:[track genre] forKey:@"genre"];
if([[track year] intValue]) [cuesheetMetadata setValue:@([[track year] intValue]) forKey:@"year"];
if([track albumGain]) [cuesheetMetadata setValue:@([track albumGain]) forKey:@"replayGainAlbumGain"];
if([track albumPeak]) [cuesheetMetadata setValue:@([track albumPeak]) forKey:@"replayGainAlbumPeak"];
if([track trackGain]) [cuesheetMetadata setValue:@([track trackGain]) forKey:@"replayGainTrackGain"];
if([track trackPeak]) [cuesheetMetadata setValue:@([track trackPeak]) forKey:@"replayGainTrackPeak"];
if([track albumGain]) [cuesheetMetadata setValue:@([track albumGain]) forKey:@"replaygain_album_gain"];
if([track albumPeak]) [cuesheetMetadata setValue:@([track albumPeak]) forKey:@"replaygain_album_peak"];
if([track trackGain]) [cuesheetMetadata setValue:@([track trackGain]) forKey:@"replaygain_track_gain"];
if([track trackPeak]) [cuesheetMetadata setValue:@([track trackPeak]) forKey:@"replaygain_track_peak"];
return [NSDictionary dictionaryWithDictionary:cuesheetMetadata];
}

View File

@ -60,23 +60,13 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence);
BOOL endOfAudio;
int metadataIndex;
NSString *artist;
NSString *albumartist;
NSString *album;
NSString *title;
NSString *genre;
NSNumber *year;
NSNumber *track;
NSNumber *disc;
float replayGainAlbumGain;
float replayGainAlbumPeak;
float replayGainTrackGain;
float replayGainTrackPeak;
float volumeScale;
int attachedPicIndex;
NSData *albumArt;
NSDictionary *metaDict;
NSDictionary *id3Metadata;
}

View File

@ -465,21 +465,9 @@ static uint8_t reverse_bits[0x100];
seekedToStart = !seekable;
artist = @"";
albumartist = @"";
album = @"";
title = @"";
genre = @"";
year = @(0);
track = @(0);
disc = @(0);
replayGainAlbumGain = 0.0;
replayGainAlbumPeak = 0.0;
replayGainTrackGain = 0.0;
replayGainTrackPeak = 0.0;
volumeScale = 1.0;
id3Metadata = [[NSDictionary alloc] init];
metaDict = [NSDictionary dictionary];
albumArt = [NSData data];
id3Metadata = @{};
metadataUpdated = NO;
[self updateMetadata];
@ -534,21 +522,18 @@ static uint8_t reverse_bits[0x100];
[self close];
}
static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) {
NSMutableArray *array = [dict valueForKey:tag];
if(!array) {
array = [[NSMutableArray alloc] init];
[dict setObject:array forKey:tag];
}
[array addObject:value];
}
- (void)updateMetadata {
NSMutableDictionary *_metaDict = [[NSMutableDictionary alloc] init];
const AVDictionaryEntry *tag = NULL;
NSString *_artist = artist;
NSString *_albumartist = albumartist;
NSString *_album = album;
NSString *_title = title;
NSString *_genre = genre;
NSNumber *_year = year;
NSNumber *_track = track;
NSNumber *_disc = disc;
float _replayGainAlbumGain = replayGainAlbumGain;
float _replayGainAlbumPeak = replayGainAlbumPeak;
float _replayGainTrackGain = replayGainTrackGain;
float _replayGainTrackPeak = replayGainTrackPeak;
float _volumeScale = volumeScale;
for(size_t i = 0; i < 2; ++i) {
AVDictionary *metadata;
if(i == 0) {
@ -567,62 +552,45 @@ static uint8_t reverse_bits[0x100];
if(!strcasecmp(tag->key, "streamtitle")) {
NSString *artistTitle = guess_encoding_of_string(tag->value);
NSArray *splitValues = [artistTitle componentsSeparatedByString:@" - "];
_artist = @"";
_title = [splitValues objectAtIndex:0];
NSString *_artist = @"";
NSString *_title = [splitValues objectAtIndex:0];
if([splitValues count] > 1) {
_artist = _title;
_title = [splitValues objectAtIndex:1];
setDictionary(_metaDict, @"artist", _artist);
setDictionary(_metaDict, @"title", _title);
} else {
setDictionary(_metaDict, @"title", _title);
}
} else if(!strcasecmp(tag->key, "icy-url")) {
_album = guess_encoding_of_string(tag->value);
} else if(!strcasecmp(tag->key, "icy-genre") ||
!strcasecmp(tag->key, "genre")) {
_genre = guess_encoding_of_string(tag->value);
} else if(!strcasecmp(tag->key, "album")) {
_album = guess_encoding_of_string(tag->value);
} else if(!strcasecmp(tag->key, "album_artist")) {
_albumartist = guess_encoding_of_string(tag->value);
} else if(!strcasecmp(tag->key, "artist")) {
_artist = guess_encoding_of_string(tag->value);
setDictionary(_metaDict, @"album", guess_encoding_of_string(tag->value));
} else if(!strcasecmp(tag->key, "icy-genre")) {
setDictionary(_metaDict, @"genre", guess_encoding_of_string(tag->value));
} else if(!strcasecmp(tag->key, "title")) {
NSString *_tag = guess_encoding_of_string(tag->value);
if(i == 0 && formatCtx->nb_chapters > 1) {
_album = _tag;
setDictionary(_metaDict, @"album", _tag);
} else {
_title = _tag;
setDictionary(_metaDict, @"title", _tag);
}
} else if(!strcasecmp(tag->key, "date") ||
!strcasecmp(tag->key, "date_recorded")) {
NSString *dateString = guess_encoding_of_string(tag->value);
_year = @([dateString intValue]);
} else if(!strcasecmp(tag->key, "track")) {
NSString *trackString = guess_encoding_of_string(tag->value);
_track = @([trackString intValue]);
} else if(!strcasecmp(tag->key, "disc")) {
NSString *discString = guess_encoding_of_string(tag->value);
_disc = @([discString intValue]);
} else if(!strcasecmp(tag->key, "replaygain_album_gain")) {
NSString *rgValue = guess_encoding_of_string(tag->value);
_replayGainAlbumGain = [rgValue floatValue];
} else if(!strcasecmp(tag->key, "replaygain_album_peak")) {
NSString *rgValue = guess_encoding_of_string(tag->value);
_replayGainAlbumPeak = [rgValue floatValue];
} else if(!strcasecmp(tag->key, "replaygain_track_gain")) {
NSString *rgValue = guess_encoding_of_string(tag->value);
_replayGainTrackGain = [rgValue floatValue];
} else if(!strcasecmp(tag->key, "replaygain_track_peak")) {
NSString *rgValue = guess_encoding_of_string(tag->value);
_replayGainTrackPeak = [rgValue floatValue];
} else if(!strcasecmp(tag->key, "date_recorded")) {
setDictionary(_metaDict, @"date", guess_encoding_of_string(tag->value));
} else if(!strcasecmp(tag->key, "replaygain_gain")) {
// global or chapter gain
NSString *rgValue = guess_encoding_of_string(tag->value);
if(i == 0) _replayGainAlbumGain = [rgValue floatValue];
else _replayGainTrackGain = [rgValue floatValue];
NSString *tagName;
if(i == 0)
tagName = @"replaygain_album_gain";
else
tagName = @"replaygain_track_gain";
setDictionary(_metaDict, tagName, guess_encoding_of_string(tag->value));
} else if(!strcasecmp(tag->key, "replaygain_peak")) {
// global or chapter peak
NSString *rgValue = guess_encoding_of_string(tag->value);
if(i == 0) _replayGainAlbumPeak = [rgValue floatValue];
else _replayGainTrackPeak = [rgValue floatValue];
NSString *tagName;
if(i == 0)
tagName = @"replaygain_album_peak";
else
tagName = @"replaygain_track_peak";
setDictionary(_metaDict, tagName, guess_encoding_of_string(tag->value));
} else if(!strcasecmp(tag->key, "iTunNORM")) {
NSString *tagString = guess_encoding_of_string(tag->value);
NSArray *tag = [tagString componentsSeparatedByString:@" "];
@ -642,8 +610,11 @@ static uint8_t reverse_bits[0x100];
float volume1 = -log10((double)(hexvalue1) / 1000) * 10;
float volume2 = -log10((double)(hexvalue2) / 1000) * 10;
float volumeToUse = MIN(volume1, volume2);
_volumeScale = pow(10, volumeToUse / 20);
NSNumber *_volumeScale = @(pow(10, volumeToUse / 20));
setDictionary(_metaDict, @"volume", [_volumeScale stringValue]);
}
} else {
setDictionary(_metaDict, guess_encoding_of_string(tag->key), guess_encoding_of_string(tag->value));
}
}
}
@ -653,39 +624,28 @@ static uint8_t reverse_bits[0x100];
HTTPSource *httpSource = (HTTPSource *)source;
if([httpSource hasMetadata]) {
NSDictionary *metadata = [httpSource metadata];
_genre = [metadata valueForKey:@"genre"];
_album = [metadata valueForKey:@"album"];
_artist = [metadata valueForKey:@"artist"];
_title = [metadata valueForKey:@"title"];
NSString *_genre = [metadata valueForKey:@"genre"];
NSString *_album = [metadata valueForKey:@"album"];
NSString *_artist = [metadata valueForKey:@"artist"];
NSString *_title = [metadata valueForKey:@"title"];
if(_genre && [_genre length]) {
[_metaDict setObject:@[_genre] forKey:@"genre"];
}
if(_album && [_album length]) {
[_metaDict setObject:@[_album] forKey:@"album"];
}
if(_artist && [_artist length]) {
[_metaDict setObject:@[_artist] forKey:@"artist"];
}
if(_title && [_title length]) {
[_metaDict setObject:@[_title] forKey:@"title"];
}
}
}
if(![_artist isEqual:artist] ||
![_albumartist isEqual:albumartist] ||
![_album isEqual:album] ||
![_title isEqual:title] ||
![_genre isEqual:genre] ||
![_year isEqual:year] ||
![_track isEqual:track] ||
![_disc isEqual:disc] ||
_replayGainAlbumGain != replayGainAlbumGain ||
_replayGainAlbumPeak != replayGainAlbumPeak ||
_replayGainTrackGain != replayGainTrackGain ||
_replayGainTrackPeak != replayGainTrackPeak ||
_volumeScale != volumeScale) {
artist = _artist;
albumartist = _albumartist;
album = _album;
title = _title;
genre = _genre;
year = _year;
track = _track;
disc = _disc;
replayGainAlbumGain = _replayGainAlbumGain;
replayGainAlbumPeak = _replayGainAlbumPeak;
replayGainTrackGain = _replayGainTrackGain;
replayGainTrackPeak = _replayGainTrackPeak;
volumeScale = _volumeScale;
if(![_metaDict isEqualToDictionary:metaDict]) {
metaDict = _metaDict;
if(![source seekable]) {
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
@ -700,7 +660,7 @@ static uint8_t reverse_bits[0x100];
Class tagReader = NSClassFromString(@"TagLibID3v2Reader");
if(tagReader && [tagReader respondsToSelector:@selector(metadataForTag:)]) {
NSDictionary *_id3Metadata = [tagReader metadataForTag:tag];
if(![_id3Metadata isEqualTo:id3Metadata]) {
if(![_id3Metadata isEqualToDictionary:id3Metadata]) {
id3Metadata = _id3Metadata;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
@ -710,7 +670,7 @@ static uint8_t reverse_bits[0x100];
- (void)updateArtwork {
NSData *_albumArt = [NSData dataWithBytes:lastReadPacket->data length:lastReadPacket->size];
if(![_albumArt isEqual:albumArt]) {
if(![_albumArt isEqualToData:albumArt]) {
albumArt = _albumArt;
if(![source seekable]) {
[self willChangeValueForKey:@"metadata"];
@ -1010,7 +970,10 @@ static uint8_t reverse_bits[0x100];
}
- (NSDictionary *)metadata {
return [NSDictionary dictionaryByMerging:@{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"replayGainAlbumGain": @(replayGainAlbumGain), @"replayGainAlbumPeak": @(replayGainAlbumPeak), @"replayGainTrackGain": @(replayGainTrackGain), @"replayGainTrackPeak": @(replayGainTrackPeak), @"volume": @(volumeScale), @"albumArt": albumArt } with:id3Metadata];
NSDictionary *dict1 = @{ @"albumArt": albumArt };
NSDictionary *dict2 = [dict1 dictionaryByMergingWith:metaDict];
NSDictionary *dict3 = [dict2 dictionaryByMergingWith:id3Metadata];
return dict3;
}
+ (NSArray *)fileTypes {

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
17C93F080B8FF67A008627D6 /* FlacDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 17C93F040B8FF67A008627D6 /* FlacDecoder.m */; };
8301C147287805F500651A6E /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8301C146287805F500651A6E /* NSDictionary+Merge.m */; };
836EF0DA27BB970B00BF35B2 /* libFLAC.8.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 836EF0D927BB970B00BF35B2 /* libFLAC.8.dylib */; };
83AA660B27B7DAE40098D4B8 /* cuesheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 83AA660A27B7DAE40098D4B8 /* cuesheet.m */; };
8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; };
@ -33,6 +34,8 @@
17C93F030B8FF67A008627D6 /* FlacDecoder.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = FlacDecoder.h; sourceTree = "<group>"; };
17C93F040B8FF67A008627D6 /* FlacDecoder.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = FlacDecoder.m; sourceTree = "<group>"; };
32DBCF630370AF2F00C91783 /* Flac_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Flac_Prefix.pch; sourceTree = "<group>"; };
8301C145287805F500651A6E /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = "<group>"; };
8301C146287805F500651A6E /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = "<group>"; };
8356BD1927B3CCBB0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = "<group>"; };
836EF0D927BB970B00BF35B2 /* libFLAC.8.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libFLAC.8.dylib; path = ../../ThirdParty/flac/lib/libFLAC.8.dylib; sourceTree = "<group>"; };
83747C4A2862DCF40021245F /* Shared.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; };
@ -89,6 +92,8 @@
08FB77AFFE84173DC02AAC07 /* Classes */ = {
isa = PBXGroup;
children = (
8301C145287805F500651A6E /* NSDictionary+Merge.h */,
8301C146287805F500651A6E /* NSDictionary+Merge.m */,
83AA660A27B7DAE40098D4B8 /* cuesheet.m */,
8356BD1927B3CCBB0074E50C /* HTTPSource.h */,
8384912D180816C900E7332D /* Logging.h */,
@ -211,6 +216,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8301C147287805F500651A6E /* NSDictionary+Merge.m in Sources */,
17C93F080B8FF67A008627D6 /* FlacDecoder.m in Sources */,
83AA660B27B7DAE40098D4B8 /* cuesheet.m in Sources */,
);

View File

@ -36,18 +36,8 @@
BOOL streamOpened;
BOOL abortFlag;
NSString *artist;
NSString *albumartist;
NSString *album;
NSString *title;
NSString *genre;
NSNumber *year;
NSNumber *track;
NSNumber *disc;
float replayGainAlbumGain;
float replayGainAlbumPeak;
float replayGainTrackGain;
float replayGainTrackPeak;
NSDictionary *metaDict;
NSDictionary *icyMetaDict;
NSData *albumArt;

View File

@ -12,6 +12,8 @@
#import "HTTPSource.h"
#import "NSDictionary+Merge.h"
extern void grabbag__cuesheet_emit(NSString **out, const FLAC__StreamMetadata *cuesheet, const char *file_reference);
@implementation FlacDecoder
@ -178,6 +180,15 @@ FLAC__StreamDecoderWriteStatus WriteCallback(const FLAC__StreamDecoder *decoder,
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}
static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) {
NSMutableArray *array = [dict valueForKey:tag];
if(!array) {
array = [[NSMutableArray alloc] init];
[dict setObject:array forKey:tag];
}
[array addObject:value];
}
// This callback is only called for STREAMINFO blocks
void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) {
// Some flacs observed in the wild have multiple STREAMINFO metadata blocks,
@ -223,18 +234,7 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta
}
if(metadata->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) {
NSString *_artist = flacDecoder->artist;
NSString *_albumartist = flacDecoder->albumartist;
NSString *_album = flacDecoder->album;
NSString *_title = flacDecoder->title;
NSString *_genre = flacDecoder->genre;
NSNumber *_year = flacDecoder->year;
NSNumber *_track = flacDecoder->track;
NSNumber *_disc = flacDecoder->disc;
float _replayGainAlbumGain = flacDecoder->replayGainAlbumGain;
float _replayGainAlbumPeak = flacDecoder->replayGainAlbumPeak;
float _replayGainTrackGain = flacDecoder->replayGainTrackGain;
float _replayGainTrackPeak = flacDecoder->replayGainTrackPeak;
NSMutableDictionary *_metaDict = [[NSMutableDictionary alloc] init];
NSString *_cuesheet = flacDecoder->cuesheet;
const FLAC__StreamMetadata_VorbisComment *vorbis_comment = &metadata->data.vorbis_comment;
for(int i = 0; i < vorbis_comment->num_comments; ++i) {
@ -246,74 +246,23 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta
free(_name);
free(_value);
name = [name lowercaseString];
if([name isEqualToString:@"artist"]) {
_artist = value;
} else if([name isEqualToString:@"albumartist"]) {
_albumartist = value;
} else if([name isEqualToString:@"album"]) {
_album = value;
} else if([name isEqualToString:@"title"]) {
_title = value;
} else if([name isEqualToString:@"genre"]) {
_genre = value;
} else if([name isEqualToString:@"cuesheet"]) {
if([name isEqualToString:@"cuesheet"]) {
_cuesheet = value;
flacDecoder->cuesheetFound = YES;
} else if([name isEqualToString:@"date"] ||
[name isEqualToString:@"year"]) {
_year = @([value intValue]);
} else if([name isEqualToString:@"tracknumber"] ||
[name isEqualToString:@"tracknum"] ||
[name isEqualToString:@"track"]) {
_track = @([value intValue]);
} else if([name isEqualToString:@"discnumber"] ||
[name isEqualToString:@"discnum"] ||
[name isEqualToString:@"disc"]) {
_disc = @([value intValue]);
} else if([name isEqualToString:@"replaygain_album_gain"]) {
_replayGainAlbumGain = [value floatValue];
} else if([name isEqualToString:@"replaygain_album_peak"]) {
_replayGainAlbumPeak = [value floatValue];
} else if([name isEqualToString:@"replaygain_track_gain"]) {
_replayGainTrackGain = [value floatValue];
} else if([name isEqualToString:@"replaygain_track_peak"]) {
_replayGainTrackPeak = [value floatValue];
} else if([name isEqualToString:@"waveformatextensible_channel_mask"]) {
if([value hasPrefix:@"0x"]) {
char *end;
const char *_value = [value UTF8String] + 2;
flacDecoder->channelConfig = (uint32_t)strtoul(_value, &end, 16);
}
} else {
setDictionary(_metaDict, name, value);
}
}
}
if(![_artist isEqual:flacDecoder->artist] ||
![_albumartist isEqual:flacDecoder->albumartist] ||
![_album isEqual:flacDecoder->album] ||
![_title isEqual:flacDecoder->title] ||
![_genre isEqual:flacDecoder->genre] ||
![_cuesheet isEqual:flacDecoder->cuesheet] ||
![_year isEqual:flacDecoder->year] ||
![_track isEqual:flacDecoder->track] ||
![_disc isEqual:flacDecoder->disc] ||
_replayGainAlbumGain != flacDecoder->replayGainAlbumGain ||
_replayGainAlbumPeak != flacDecoder->replayGainAlbumPeak ||
_replayGainTrackGain != flacDecoder->replayGainTrackGain ||
_replayGainTrackPeak != flacDecoder->replayGainTrackPeak) {
flacDecoder->artist = _artist;
flacDecoder->albumartist = _albumartist;
flacDecoder->album = _album;
flacDecoder->title = _title;
flacDecoder->genre = _genre;
flacDecoder->cuesheet = _cuesheet;
flacDecoder->year = _year;
flacDecoder->track = _track;
flacDecoder->disc = _disc;
flacDecoder->replayGainAlbumGain = _replayGainAlbumGain;
flacDecoder->replayGainAlbumPeak = _replayGainAlbumPeak;
flacDecoder->replayGainTrackGain = _replayGainTrackGain;
flacDecoder->replayGainTrackPeak = _replayGainTrackPeak;
if(![_metaDict isEqualToDictionary:flacDecoder->metaDict]) {
flacDecoder->metaDict = _metaDict;
if(![flacDecoder->source seekable]) {
[flacDecoder willChangeValueForKey:@"metadata"];
@ -348,18 +297,8 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
isOggFlac = YES;
}
artist = @"";
albumartist = @"";
album = @"";
title = @"";
genre = @"";
year = @(0);
track = @(0);
disc = @(0);
replayGainAlbumGain = 0.0;
replayGainAlbumPeak = 0.0;
replayGainTrackGain = 0.0;
replayGainTrackPeak = 0.0;
metaDict = [NSDictionary dictionary];
icyMetaDict = [NSDictionary dictionary];
albumArt = [NSData data];
cuesheetFound = NO;
cuesheet = @"";
@ -461,20 +400,28 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
Class sourceClass = [source class];
if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) {
HTTPSource *httpSource = (HTTPSource *)source;
NSMutableDictionary *_icyMetaDict = [[NSMutableDictionary alloc] init];
if([httpSource hasMetadata]) {
NSDictionary *metadata = [httpSource metadata];
NSString *_genre = [metadata valueForKey:@"genre"];
NSString *_album = [metadata valueForKey:@"album"];
NSString *_artist = [metadata valueForKey:@"artist"];
NSString *_title = [metadata valueForKey:@"title"];
if(![_genre isEqualToString:genre] ||
![_album isEqualToString:album] ||
![_artist isEqualToString:artist] ||
![_title isEqualToString:title]) {
genre = _genre;
album = _album;
artist = _artist;
title = _title;
if(_genre && [_genre length]) {
setDictionary(_icyMetaDict, @"genre", _genre);
}
if(_album && [_album length]) {
setDictionary(_icyMetaDict, @"album", _album);
}
if(_artist && [_artist length]) {
setDictionary(_icyMetaDict, @"artist", _artist);
}
if(_title && [_title length]) {
setDictionary(_icyMetaDict, @"title", _title);
}
if(![_icyMetaDict isEqualToDictionary:icyMetaDict]) {
icyMetaDict = _icyMetaDict;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
@ -557,7 +504,10 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
}
- (NSDictionary *)metadata {
return @{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"replayGainAlbumGain": @(replayGainAlbumGain), @"replayGainAlbumPeak": @(replayGainAlbumPeak), @"replayGainTrackGain": @(replayGainTrackGain), @"replayGainTrackPeak": @(replayGainTrackPeak), @"cuesheet": cuesheet, @"albumArt": albumArt };
NSDictionary *dict1 = @{ @"albumArt": albumArt, @"cuesheet": cuesheet };
NSDictionary *dict2 = [dict1 dictionaryByMergingWith:metaDict];
NSDictionary *dict3 = [dict2 dictionaryByMergingWith:icyMetaDict];
return dict3;
}
+ (NSArray *)fileTypes {

View File

@ -24,12 +24,6 @@
int tagLengthMs;
int tagFadeMs;
float replayGainAlbumGain;
float replayGainAlbumPeak;
float replayGainTrackGain;
float replayGainTrackPeak;
float volume;
int type;
int sampleRate;

View File

@ -220,6 +220,20 @@ static int parse_time_crap(NSString *value) {
return totalSeconds;
}
static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) {
NSMutableArray *array = [dict valueForKey:tag];
if(!array) {
array = [[NSMutableArray alloc] init];
[dict setObject:array forKey:tag];
}
if([array count]) {
NSString *existing = array[0];
array[0] = [existing stringByAppendingFormat:@"\r\n%@", value];
} else {
[array addObject:value];
}
}
static int psf_info_meta(void *context, const char *name, const char *value) {
struct psf_info_meta_state *state = (struct psf_info_meta_state *)context;
@ -232,46 +246,18 @@ static int psf_info_meta(void *context, const char *name, const char *value) {
if([taglc isEqualToString:@"game"]) {
taglc = @"album";
} else if([taglc isEqualToString:@"date"]) {
taglc = @"year";
} else if([taglc isEqualToString:@"album artist"]) {
taglc = @"albumartist";
} else if([taglc isEqualToString:@"tracknumber"]) {
taglc = @"track";
} else if([taglc isEqualToString:@"discnumber"]) {
taglc = @"disc";
}
if([taglc hasPrefix:@"replaygain_"]) {
if([taglc hasPrefix:@"replaygain_album_"]) {
if([taglc hasSuffix:@"gain"])
state->albumGain = [svalue floatValue];
else if([taglc hasSuffix:@"peak"])
state->albumPeak = [svalue floatValue];
} else if([taglc hasPrefix:@"replaygain_track_"]) {
if([taglc hasSuffix:@"gain"])
state->trackGain = [svalue floatValue];
else if([taglc hasSuffix:@"peak"])
state->trackPeak = [svalue floatValue];
}
} else if([taglc isEqualToString:@"volume"]) {
state->volume = [svalue floatValue];
} else if([taglc isEqualToString:@"length"]) {
if([taglc isEqualToString:@"length"]) {
state->tag_length_ms = parse_time_crap(svalue);
setDictionary(state->info, @"psf_length", svalue);
} else if([taglc isEqualToString:@"fade"]) {
state->tag_fade_ms = parse_time_crap(svalue);
setDictionary(state->info, @"psf_fade", svalue);
} else if([taglc isEqualToString:@"utf8"]) {
state->utf8 = true;
} else if([taglc isEqualToString:@"title"] ||
[taglc isEqualToString:@"albumartist"] ||
[taglc isEqualToString:@"artist"] ||
[taglc isEqualToString:@"album"] ||
[taglc isEqualToString:@"genre"]) {
[state->info setObject:svalue forKey:taglc];
} else if([taglc isEqualToString:@"year"] ||
[taglc isEqualToString:@"track"] ||
[taglc isEqualToString:@"disc"]) {
[state->info setObject:@([svalue intValue]) forKey:taglc];
} else {
setDictionary(state->info, taglc, svalue);
}
return 0;
@ -1218,12 +1204,6 @@ static int usf_info(void *context, const char *name, const char *value) {
tagFadeMs = (int)ceil(defaultFade * 1000.0);
}
replayGainAlbumGain = info.albumGain;
replayGainAlbumPeak = info.albumPeak;
replayGainTrackGain = info.trackGain;
replayGainTrackPeak = info.trackPeak;
volume = info.volume;
metadataList = info.info;
framesLength = [self retrieveFrameCount:tagLengthMs];
@ -1573,11 +1553,6 @@ static int usf_info(void *context, const char *name, const char *value) {
@"totalFrames": @(totalFrames),
@"bitrate": @(0),
@"seekable": @(YES),
@"replayGainAlbumGain": @(replayGainAlbumGain),
@"replayGainAlbumPeak": @(replayGainAlbumPeak),
@"replayGainTrackGain": @(replayGainTrackGain),
@"replayGainTrackPeak": @(replayGainTrackPeak),
@"volume": @(volume),
@"codec": codec,
@"endian": @"host",
@"encoding": @"synthesized" };

View File

@ -26,6 +26,15 @@
return 1.0f;
}
static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) {
NSMutableArray *array = [dict valueForKey:tag];
if(!array) {
array = [[NSMutableArray alloc] init];
[dict setObject:array forKey:tag];
}
[array addObject:value];
}
+ (NSDictionary *)metadataForURL:(NSURL *)url {
id audioSourceClass = NSClassFromString(@"AudioSource");
id<CogSource> source = [audioSourceClass audioSourceForURL:url];
@ -62,9 +71,7 @@
midi_meta_data_item item;
bool remap_display_name = !metadata.get_item("title", item);
NSArray *allowedKeys = @[@"title", @"artist", @"albumartist", @"album", @"genre"];
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:10];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
for(size_t i = 0; i < metadata.get_count(); ++i) {
const midi_meta_data_item &item = metadata[i];
@ -72,11 +79,7 @@
if(![name isEqualToString:@"type"]) {
if(remap_display_name && [name isEqualToString:@"display_name"])
name = @"title";
if([allowedKeys containsObject:name]) {
[dict setObject:guess_encoding_of_string(item.m_value.c_str()) forKey:name];
} else if([name isEqualToString:@"year"]) {
[dict setObject:@([guess_encoding_of_string(item.m_value.c_str()) intValue]) forKey:name];
}
setDictionary(dict, name, guess_encoding_of_string(item.m_value.c_str()));
}
}

View File

@ -27,6 +27,15 @@
return 1.0f;
}
static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) {
NSMutableArray *array = [dict valueForKey:tag];
if(!array) {
array = [[NSMutableArray alloc] init];
[dict setObject:array forKey:tag];
}
[array addObject:value];
}
+ (NSDictionary *)metadataForURL:(NSURL *)url {
id audioSourceClass = NSClassFromString(@"AudioSource");
id<CogSource> source = [audioSourceClass audioSourceForURL:url];
@ -55,41 +64,25 @@
std::map<std::string, std::string> ctls;
openmpt::module *mod = new openmpt::module(data, std::clog, ctls);
NSString *title = nil;
NSString *artist = nil;
// NSString * comment = nil;
NSString *date = nil;
NSString *type = nil;
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
std::vector<std::string> keys = mod->get_metadata_keys();
for(std::vector<std::string>::iterator key = keys.begin(); key != keys.end(); ++key) {
if(*key == "title")
title = guess_encoding_of_string(mod->get_metadata(*key).c_str());
else if(*key == "artist")
artist = guess_encoding_of_string(mod->get_metadata(*key).c_str());
/*else if ( *key == "message" )
comment = guess_encoding_of_string(mod->get_metadata( *key ).c_str());*/
else if(*key == "date")
date = guess_encoding_of_string(mod->get_metadata(*key).c_str());
else if(*key == "type_long")
type = guess_encoding_of_string(mod->get_metadata(*key).c_str());
NSString *tag = guess_encoding_of_string((*key).c_str());
NSString *value = guess_encoding_of_string(mod->get_metadata(*key).c_str());
if(*key == "type")
continue;
else if(*key == "type_long") {
setDictionary(dict, @"codec", value);
} else {
setDictionary(dict, tag, value);
}
}
delete mod;
if(title == nil)
title = @"";
if(artist == nil)
artist = @"";
/*if (comment == nil)
comment = @"";*/
if(date == nil)
date = @"";
if(type == nil)
type = @"";
return @{ @"title": title, @"artist": artist, /*@"comment": comment,*/ @"year": @([date intValue]), @"codec": type };
return dict;
} catch(std::exception & /*e*/) {
return 0;
}

View File

@ -23,23 +23,13 @@
int channels;
long totalFrames;
NSString *artist;
NSString *albumartist;
NSString *album;
NSString *title;
NSString *genre;
NSNumber *year;
NSNumber *track;
NSNumber *disc;
float replayGainAlbumGain;
float replayGainTrackGain;
NSString *icygenre;
NSString *icyalbum;
NSString *icyartist;
NSString *icytitle;
NSDictionary *metaDict;
NSDictionary *icyMetaDict;
NSData *albumArt;
float replayGainAlbumGain;
float replayGainTrackGain;
}
@end

View File

@ -16,6 +16,8 @@
#import "NSDictionary+Merge.h"
#import <FLAC/metadata.h>
@implementation OpusFile
static const int MAXCHANNELS = 8;
@ -121,113 +123,60 @@ opus_int64 sourceTell(void *_stream) {
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
artist = @"";
albumartist = @"";
album = @"";
title = @"";
genre = @"";
icygenre = @"";
icyalbum = @"";
icyartist = @"";
icytitle = @"";
year = @(0);
track = @(0);
disc = @(0);
metaDict = [NSDictionary dictionary];
icyMetaDict = [NSDictionary dictionary];
albumArt = [NSData data];
[self updateMetadata];
return YES;
}
- (NSString *)parseTag:(NSString *)tag fromTags:(const OpusTags *)tags {
NSMutableArray *tagStrings = [[NSMutableArray alloc] init];
int tagCount = opus_tags_query_count(tags, [tag UTF8String]);
for(int i = 0; i < tagCount; ++i) {
const char *value = opus_tags_query(tags, [tag UTF8String], i);
[tagStrings addObject:guess_encoding_of_string(value)];
static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) {
NSMutableArray *array = [dict valueForKey:tag];
if(!array) {
array = [[NSMutableArray alloc] init];
[dict setObject:array forKey:tag];
}
return [tagStrings componentsJoinedByString:@", "];
[array addObject:value];
}
- (void)updateMetadata {
const OpusTags *tags = op_tags(opusRef, -1);
const struct OpusTags *tags = op_tags(opusRef, -1);
NSMutableDictionary *_metaDict = [[NSMutableDictionary alloc] init];
NSData *_albumArt = albumArt;
if(tags) {
NSString *_artist = [self parseTag:@"artist" fromTags:tags];
NSString *_albumartist = [self parseTag:@"albumartist" fromTags:tags];
NSString *_album = [self parseTag:@"album" fromTags:tags];
NSString *_title = [self parseTag:@"title" fromTags:tags];
NSString *_genre = [self parseTag:@"genre" fromTags:tags];
for(int i = 0; i < tags->comments; ++i) {
FLAC__StreamMetadata_VorbisComment_Entry entry = { .entry = (FLAC__byte *)tags->user_comments[i], .length = tags->comment_lengths[i] };
char *name, *value;
if(FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair(entry, &name, &value)) {
NSString *tagName = guess_encoding_of_string(name);
NSString *tagValue = guess_encoding_of_string(value);
free(name);
free(value);
NSString *_yearDate = [self parseTag:@"date" fromTags:tags];
NSString *_yearYear = [self parseTag:@"year" fromTags:tags];
tagName = [tagName lowercaseString];
NSNumber *_year = @(0);
if([_yearDate length])
_year = @([_yearDate intValue]);
else if([_yearYear length])
_year = @([_yearYear intValue]);
NSString *_trackNumber = [self parseTag:@"tracknumber" fromTags:tags];
NSString *_trackNum = [self parseTag:@"tracknum" fromTags:tags];
NSString *_trackTrack = [self parseTag:@"track" fromTags:tags];
NSNumber *_track = @(0);
if([_trackNumber length])
_track = @([_trackNumber intValue]);
else if([_trackNum length])
_track = @([_trackNum intValue]);
else if([_trackTrack length])
_track = @([_trackTrack intValue]);
NSString *_discNumber = [self parseTag:@"discnumber" fromTags:tags];
NSString *_discNum = [self parseTag:@"discnum" fromTags:tags];
NSString *_discDisc = [self parseTag:@"disc" fromTags:tags];
NSNumber *_disc = @(0);
if([_discNumber length])
_disc = @([_discNumber intValue]);
else if([_discNum length])
_disc = @([_discNum intValue]);
else if([_discDisc length])
_disc = @([_discDisc intValue]);
NSData *_albumArt = [NSData data];
size_t count = opus_tags_query_count(tags, "METADATA_BLOCK_PICTURE");
if(count) {
const char *pictureTag = opus_tags_query(tags, "METADATA_BLOCK_PICTURE", 0);
OpusPictureTag _pic = { 0 };
if(opus_picture_tag_parse(&_pic, pictureTag) >= 0) {
if(_pic.format == OP_PIC_FORMAT_PNG ||
_pic.format == OP_PIC_FORMAT_JPEG ||
_pic.format == OP_PIC_FORMAT_GIF) {
_albumArt = [NSData dataWithBytes:_pic.data length:_pic.data_length];
if([tagName isEqualToString:@"metadata_block_picture"]) {
OpusPictureTag _pic = { 0 };
if(opus_picture_tag_parse(&_pic, [tagValue UTF8String]) >= 0) {
if(_pic.format == OP_PIC_FORMAT_PNG ||
_pic.format == OP_PIC_FORMAT_JPEG ||
_pic.format == OP_PIC_FORMAT_GIF) {
_albumArt = [NSData dataWithBytes:_pic.data length:_pic.data_length];
}
opus_picture_tag_clear(&_pic);
}
} else {
setDictionary(_metaDict, tagName, tagValue);
}
opus_picture_tag_clear(&_pic);
}
}
if(![_artist isEqual:artist] ||
![_albumartist isEqual:albumartist] ||
![_album isEqual:album] ||
![_title isEqual:title] ||
![_genre isEqual:genre] ||
![_year isEqual:year] ||
![_track isEqual:year] ||
![_disc isEqual:disc] ||
![_albumArt isEqual:albumArt]) {
artist = _artist;
albumartist = _albumartist;
album = _album;
title = _title;
genre = _genre;
year = _year;
track = _track;
disc = _disc;
if(![_albumArt isEqualToData:albumArt] ||
![_metaDict isEqualToDictionary:metaDict]) {
metaDict = _metaDict;
albumArt = _albumArt;
[self willChangeValueForKey:@"metadata"];
@ -239,31 +188,35 @@ opus_int64 sourceTell(void *_stream) {
- (void)updateIcyMetadata {
if([source seekable]) return;
NSString *_genre = icygenre;
NSString *_album = icyalbum;
NSString *_artist = icyartist;
NSString *_title = icytitle;
NSMutableDictionary *_icyMetaDict = [[NSMutableDictionary alloc] init];
Class sourceClass = [source class];
if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) {
HTTPSource *httpSource = (HTTPSource *)source;
if([httpSource hasMetadata]) {
NSDictionary *metadata = [httpSource metadata];
_genre = [metadata valueForKey:@"genre"];
_album = [metadata valueForKey:@"album"];
_artist = [metadata valueForKey:@"artist"];
_title = [metadata valueForKey:@"title"];
NSString *_genre = [metadata valueForKey:@"genre"];
NSString *_album = [metadata valueForKey:@"album"];
NSString *_artist = [metadata valueForKey:@"artist"];
NSString *_title = [metadata valueForKey:@"title"];
if(_genre && [_genre length]) {
setDictionary(_icyMetaDict, @"genre", _genre);
}
if(_album && [_album length]) {
setDictionary(_icyMetaDict, @"album", _album);
}
if(_artist && [_artist length]) {
setDictionary(_icyMetaDict, @"artist", _artist);
}
if(_title && [_title length]) {
setDictionary(_icyMetaDict, @"title", _title);
}
}
}
if(![_genre isEqual:icygenre] ||
![_album isEqual:icyalbum] ||
![_artist isEqual:icyartist] ||
![_title isEqual:icytitle]) {
icygenre = _genre;
icyalbum = _album;
icyartist = _artist;
icytitle = _title;
if(![_icyMetaDict isEqualToDictionary:icyMetaDict]) {
icyMetaDict = _icyMetaDict;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
@ -338,15 +291,18 @@ opus_int64 sourceTell(void *_stream) {
@"totalFrames": @(totalFrames),
@"bitrate": @(bitrate),
@"seekable": @([source seekable] && seekable),
@"replayGainAlbumGain": @(replayGainAlbumGain),
@"replayGainTrackGain": @(replayGainTrackGain),
@"replaygain_album_gain": @(replayGainAlbumGain),
@"replaygain_track_gain": @(replayGainTrackGain),
@"codec": @"Opus",
@"endian": @"host",
@"encoding": @"lossy" };
}
- (NSDictionary *)metadata {
return [@{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"albumArt": albumArt } dictionaryByMergingWith:@{ @"genre": icygenre, @"album": icyalbum, @"artist": icyartist, @"title": icytitle }];
NSDictionary *dict1 = @{ @"albumArt": albumArt };
NSDictionary *dict2 = [dict1 dictionaryByMergingWith:metaDict];
NSDictionary *dict3 = [dict2 dictionaryByMergingWith:icyMetaDict];
return dict3;
}
+ (NSArray *)fileTypes {

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
8301C14928780C1A00651A6E /* libFLAC.8.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 8301C14828780C1A00651A6E /* libFLAC.8.dylib */; };
83186316285CEC91001422CC /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 83186315285CEC91001422CC /* NSDictionary+Merge.m */; };
836EF0CF27BB952F00BF35B2 /* libopusfile.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 836EF0CE27BB952F00BF35B2 /* libopusfile.0.dylib */; };
8375B04017FFEA400092A79F /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8375B03F17FFEA400092A79F /* Cocoa.framework */; };
@ -27,6 +28,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
8301C14828780C1A00651A6E /* libFLAC.8.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libFLAC.8.dylib; path = ../../ThirdParty/flac/lib/libFLAC.8.dylib; sourceTree = "<group>"; };
83186314285CEC91001422CC /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../../Utils/NSDictionary+Merge.h"; sourceTree = "<group>"; };
83186315285CEC91001422CC /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../../Utils/NSDictionary+Merge.m"; sourceTree = "<group>"; };
833F68411CDBCABC00AFB9F0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -53,6 +55,7 @@
buildActionMask = 2147483647;
files = (
8375B04017FFEA400092A79F /* Cocoa.framework in Frameworks */,
8301C14928780C1A00651A6E /* libFLAC.8.dylib in Frameworks */,
836EF0CF27BB952F00BF35B2 /* libopusfile.0.dylib in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -90,6 +93,7 @@
8375B03E17FFEA400092A79F /* Frameworks */ = {
isa = PBXGroup;
children = (
8301C14828780C1A00651A6E /* libFLAC.8.dylib */,
836EF0CE27BB952F00BF35B2 /* libopusfile.0.dylib */,
8375B03F17FFEA400092A79F /* Cocoa.framework */,
8375B04117FFEA400092A79F /* Other Frameworks */,
@ -340,10 +344,14 @@
../../ThirdParty/opusfile/include,
../../ThirdParty/opus/include,
../../ThirdParty/ogg/include,
../../ThirdParty/flac/include,
);
INFOPLIST_FILE = "Opus/OpusPlugin-Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
LIBRARY_SEARCH_PATHS = ../../ThirdParty/opusfile/lib;
LIBRARY_SEARCH_PATHS = (
../../ThirdParty/opusfile/lib,
../../ThirdParty/flac/lib,
);
PRODUCT_BUNDLE_IDENTIFIER = net.kode54.opus;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
@ -364,10 +372,14 @@
../../ThirdParty/opusfile/include,
../../ThirdParty/opus/include,
../../ThirdParty/ogg/include,
../../ThirdParty/flac/include,
);
INFOPLIST_FILE = "Opus/OpusPlugin-Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
LIBRARY_SEARCH_PATHS = ../../ThirdParty/opusfile/lib;
LIBRARY_SEARCH_PATHS = (
../../ThirdParty/opusfile/lib,
../../ThirdParty/flac/lib,
);
PRODUCT_BUNDLE_IDENTIFIER = net.kode54.opus;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;

View File

@ -87,10 +87,10 @@
rgAlbumPeak = tag->rgAlbumPeak();
rgTrackGain = tag->rgTrackGain();
rgTrackPeak = tag->rgTrackPeak();
[dict setObject:@(rgAlbumGain) forKey:@"replayGainAlbumGain"];
[dict setObject:@(rgAlbumPeak) forKey:@"replayGainAlbumPeak"];
[dict setObject:@(rgTrackGain) forKey:@"replayGainTrackGain"];
[dict setObject:@(rgTrackPeak) forKey:@"replayGainTrackPeak"];
[dict setObject:@(rgAlbumGain) forKey:@"replaygain_album_gain"];
[dict setObject:@(rgAlbumPeak) forKey:@"replaygain_album_peak"];
[dict setObject:@(rgTrackGain) forKey:@"replaygain_track_gain"];
[dict setObject:@(rgTrackPeak) forKey:@"replaygain_track_peak"];
soundcheck = tag->soundcheck();
if(!soundcheck.isEmpty()) {
@ -128,6 +128,9 @@
if(!cuesheet.isEmpty())
[dict setObject:[NSString stringWithUTF8String:cuesheet.toCString(true)] forKey:@"cuesheet"];
if(!comment.isEmpty())
[dict setObject:[NSString stringWithUTF8String:comment.toCString(true)] forKey:@"comment"];
// Try to load the image.
NSData *image = nil;

View File

@ -101,10 +101,10 @@
rgAlbumPeak = tag->rgAlbumPeak();
rgTrackGain = tag->rgTrackGain();
rgTrackPeak = tag->rgTrackPeak();
[dict setObject:@(rgAlbumGain) forKey:@"replayGainAlbumGain"];
[dict setObject:@(rgAlbumPeak) forKey:@"replayGainAlbumPeak"];
[dict setObject:@(rgTrackGain) forKey:@"replayGainTrackGain"];
[dict setObject:@(rgTrackPeak) forKey:@"replayGainTrackPeak"];
[dict setObject:@(rgAlbumGain) forKey:@"replaygain_album_gain"];
[dict setObject:@(rgAlbumPeak) forKey:@"replaygain_album_peak"];
[dict setObject:@(rgTrackGain) forKey:@"replaygain_track_gain"];
[dict setObject:@(rgTrackPeak) forKey:@"replaygain_track_peak"];
soundcheck = tag->soundcheck();
if(!soundcheck.isEmpty()) {
@ -141,6 +141,9 @@
if(!cuesheet.isEmpty())
[dict setObject:[NSString stringWithUTF8String:cuesheet.toCString(true)] forKey:@"cuesheet"];
if(!comment.isEmpty())
[dict setObject:[NSString stringWithUTF8String:comment.toCString(true)] forKey:@"comment"];
}
// Try to load the image.

View File

@ -32,23 +32,8 @@
float frequency;
long totalFrames;
NSString *artist;
NSString *albumartist;
NSString *album;
NSString *title;
NSString *genre;
NSNumber *year;
NSNumber *track;
NSNumber *disc;
float replayGainAlbumGain;
float replayGainAlbumPeak;
float replayGainTrackGain;
float replayGainTrackPeak;
NSString *icygenre;
NSString *icyalbum;
NSString *icyartist;
NSString *icytitle;
NSDictionary *metaDict;
NSDictionary *icyMetaDict;
NSData *albumArt;
}

View File

@ -16,6 +16,8 @@
#import "NSDictionary+Merge.h"
#import <FLAC/metadata.h>
@implementation VorbisDecoder
static const int MAXCHANNELS = 8;
@ -101,111 +103,58 @@ long sourceTell(void *datasource) {
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
artist = @"";
albumartist = @"";
album = @"";
title = @"";
genre = @"";
icygenre = @"";
icyalbum = @"";
icyartist = @"";
icytitle = @"";
year = @(0);
track = @(0);
disc = @(0);
metaDict = [NSDictionary dictionary];
icyMetaDict = [NSDictionary dictionary];
albumArt = [NSData data];
[self updateMetadata];
return YES;
}
- (NSString *)parseTag:(NSString *)tag fromTags:(vorbis_comment *)tags {
NSMutableArray *tagStrings = [[NSMutableArray alloc] init];
int tagCount = vorbis_comment_query_count(tags, [tag UTF8String]);
for(int i = 0; i < tagCount; ++i) {
const char *value = vorbis_comment_query(tags, [tag UTF8String], i);
[tagStrings addObject:guess_encoding_of_string(value)];
static void setDictionary(NSMutableDictionary *dict, NSString *tag, NSString *value) {
NSMutableArray *array = [dict valueForKey:tag];
if(!array) {
array = [[NSMutableArray alloc] init];
[dict setObject:array forKey:tag];
}
return [tagStrings componentsJoinedByString:@", "];
[array addObject:value];
}
- (void)updateMetadata {
vorbis_comment *tags = ov_comment(&vorbisRef, -1);
NSMutableDictionary *_metaDict = [[NSMutableDictionary alloc] init];
NSData *_albumArt = albumArt;
if(tags) {
NSString *_artist = [self parseTag:@"artist" fromTags:tags];
NSString *_albumartist = [self parseTag:@"albumartist" fromTags:tags];
NSString *_album = [self parseTag:@"album" fromTags:tags];
NSString *_title = [self parseTag:@"title" fromTags:tags];
NSString *_genre = [self parseTag:@"genre" fromTags:tags];
for(int i = 0; i < tags->comments; ++i) {
FLAC__StreamMetadata_VorbisComment_Entry entry = { .entry = (FLAC__byte *)tags->user_comments[i], .length = tags->comment_lengths[i] };
char *name, *value;
if(FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair(entry, &name, &value)) {
NSString *tagName = guess_encoding_of_string(name);
NSString *tagValue = guess_encoding_of_string(value);
free(name);
free(value);
NSString *_yearDate = [self parseTag:@"date" fromTags:tags];
NSString *_yearYear = [self parseTag:@"year" fromTags:tags];
tagName = [tagName lowercaseString];
NSNumber *_year = @(0);
if([_yearDate length])
_year = @([_yearDate intValue]);
else if([_yearYear length])
_year = @([_yearYear intValue]);
NSString *_trackNumber = [self parseTag:@"tracknumber" fromTags:tags];
NSString *_trackNum = [self parseTag:@"tracknum" fromTags:tags];
NSString *_trackTrack = [self parseTag:@"track" fromTags:tags];
NSNumber *_track = @(0);
if([_trackNumber length])
_track = @([_trackNumber intValue]);
else if([_trackNum length])
_track = @([_trackNum intValue]);
else if([_trackTrack length])
_track = @([_trackTrack intValue]);
NSString *_discNumber = [self parseTag:@"discnumber" fromTags:tags];
NSString *_discNum = [self parseTag:@"discnum" fromTags:tags];
NSString *_discDisc = [self parseTag:@"disc" fromTags:tags];
NSNumber *_disc = @(0);
if([_discNumber length])
_disc = @([_discNumber intValue]);
else if([_discNum length])
_disc = @([_discNum intValue]);
else if([_discDisc length])
_disc = @([_discDisc intValue]);
NSData *_albumArt = [NSData data];
size_t count = vorbis_comment_query_count(tags, "METADATA_BLOCK_PICTURE");
if(count) {
const char *pictureTag = vorbis_comment_query(tags, "METADATA_BLOCK_PICTURE", 0);
flac_picture_t *picture = flac_picture_parse_from_base64(pictureTag);
if(picture) {
if(picture->binary && picture->binary_length) {
_albumArt = [NSData dataWithBytes:picture->binary length:picture->binary_length];
if([tagName isEqualToString:@"metadata_block_picture"]) {
flac_picture_t *picture = flac_picture_parse_from_base64([tagValue UTF8String]);
if(picture) {
if(picture->binary && picture->binary_length) {
_albumArt = [NSData dataWithBytes:picture->binary length:picture->binary_length];
}
flac_picture_free(picture);
}
} else {
setDictionary(_metaDict, tagName, tagValue);
}
flac_picture_free(picture);
}
}
if(![_artist isEqual:artist] ||
![_albumartist isEqual:albumartist] ||
![_album isEqual:album] ||
![_title isEqual:title] ||
![_genre isEqual:genre] ||
![_year isEqual:year] ||
![_track isEqual:year] ||
![_disc isEqual:disc] ||
![_albumArt isEqual:albumArt]) {
artist = _artist;
albumartist = _albumartist;
album = _album;
title = _title;
genre = _genre;
year = _year;
track = _track;
disc = _disc;
if(![_albumArt isEqualToData:albumArt] ||
![_metaDict isEqualToDictionary:metaDict]) {
metaDict = _metaDict;
albumArt = _albumArt;
[self willChangeValueForKey:@"metadata"];
@ -217,31 +166,35 @@ long sourceTell(void *datasource) {
- (void)updateIcyMetadata {
if([source seekable]) return;
NSString *_genre = icygenre;
NSString *_album = icyalbum;
NSString *_artist = icyartist;
NSString *_title = icytitle;
NSMutableDictionary *_icyMetaDict = [[NSMutableDictionary alloc] init];
Class sourceClass = [source class];
if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) {
HTTPSource *httpSource = (HTTPSource *)source;
if([httpSource hasMetadata]) {
NSDictionary *metadata = [httpSource metadata];
_genre = [metadata valueForKey:@"genre"];
_album = [metadata valueForKey:@"album"];
_artist = [metadata valueForKey:@"artist"];
_title = [metadata valueForKey:@"title"];
NSString *_genre = [metadata valueForKey:@"genre"];
NSString *_album = [metadata valueForKey:@"album"];
NSString *_artist = [metadata valueForKey:@"artist"];
NSString *_title = [metadata valueForKey:@"title"];
if(_genre && [_genre length]) {
setDictionary(_icyMetaDict, @"genre", _genre);
}
if(_album && [_album length]) {
setDictionary(_icyMetaDict, @"album", _album);
}
if(_artist && [_artist length]) {
setDictionary(_icyMetaDict, @"artist", _artist);
}
if(_title && [_title length]) {
setDictionary(_icyMetaDict, @"title", _title);
}
}
}
if(![_genre isEqual:icygenre] ||
![_album isEqual:icyalbum] ||
![_artist isEqual:icyartist] ||
![_title isEqual:icytitle]) {
icygenre = _genre;
icyalbum = _album;
icyartist = _artist;
icytitle = _title;
if(![_icyMetaDict isEqualToDictionary:icyMetaDict]) {
icyMetaDict = _icyMetaDict;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
@ -325,7 +278,10 @@ long sourceTell(void *datasource) {
}
- (NSDictionary *)metadata {
return [@{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"albumArt": albumArt } dictionaryByMergingWith:@{ @"genre": icygenre, @"album": icyalbum, @"artist": icyartist, @"title": icytitle }];
NSDictionary *dict1 = @{ @"albumArt": albumArt };
NSDictionary *dict2 = [dict1 dictionaryByMergingWith:metaDict];
NSDictionary *dict3 = [dict2 dictionaryByMergingWith:icyMetaDict];
return dict3;
}
+ (NSArray *)fileTypes {

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
17C93D360B8FDA66008627D6 /* VorbisDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 17C93D340B8FDA66008627D6 /* VorbisDecoder.m */; };
8301C14B287810F300651A6E /* libFLAC.8.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 8301C14A287810F300651A6E /* libFLAC.8.dylib */; };
83186313285CEBD2001422CC /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 83186312285CEBD2001422CC /* NSDictionary+Merge.m */; };
836EF0D627BB969D00BF35B2 /* libvorbisfile.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 836EF0D427BB969D00BF35B2 /* libvorbisfile.3.dylib */; };
836EF0DF27BB987000BF35B2 /* libvorbis.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 836EF0DE27BB987000BF35B2 /* libvorbis.0.dylib */; };
@ -24,6 +25,7 @@
17C93D330B8FDA66008627D6 /* VorbisDecoder.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = VorbisDecoder.h; sourceTree = "<group>"; };
17C93D340B8FDA66008627D6 /* VorbisDecoder.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = VorbisDecoder.m; sourceTree = "<group>"; };
32DBCF630370AF2F00C91783 /* VorbisPlugin_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VorbisPlugin_Prefix.pch; sourceTree = "<group>"; };
8301C14A287810F300651A6E /* libFLAC.8.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libFLAC.8.dylib; path = ../../ThirdParty/flac/lib/libFLAC.8.dylib; sourceTree = "<group>"; };
83186311285CEBD2001422CC /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = "<group>"; };
83186312285CEBD2001422CC /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = "<group>"; };
8356BD1C27B46A2D0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = "<group>"; };
@ -45,6 +47,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8301C14B287810F300651A6E /* libFLAC.8.dylib in Frameworks */,
8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */,
836EF0DF27BB987000BF35B2 /* libvorbis.0.dylib in Frameworks */,
836EF0D627BB969D00BF35B2 /* libvorbisfile.3.dylib in Frameworks */,
@ -103,6 +106,7 @@
1058C7ACFEA557BF11CA2CBB /* Linked Frameworks */ = {
isa = PBXGroup;
children = (
8301C14A287810F300651A6E /* libFLAC.8.dylib */,
836EF0DE27BB987000BF35B2 /* libvorbis.0.dylib */,
836EF0D427BB969D00BF35B2 /* libvorbisfile.3.dylib */,
1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */,
@ -295,10 +299,14 @@
"$(inherited)",
../../ThirdParty/vorbis/include,
../../ThirdParty/ogg/include,
../../ThirdParty/flac/include,
);
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "$(HOME)/Library/Bundles";
LIBRARY_SEARCH_PATHS = ../../ThirdParty/vorbis/lib;
LIBRARY_SEARCH_PATHS = (
../../ThirdParty/vorbis/lib,
../../ThirdParty/flac/lib,
);
PRODUCT_BUNDLE_IDENTIFIER = org.cogx.vorbis;
PRODUCT_NAME = VorbisPlugin;
SDKROOT = macosx;
@ -324,10 +332,14 @@
"$(inherited)",
../../ThirdParty/vorbis/include,
../../ThirdParty/ogg/include,
../../ThirdParty/flac/include,
);
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "$(HOME)/Library/Bundles";
LIBRARY_SEARCH_PATHS = ../../ThirdParty/vorbis/lib;
LIBRARY_SEARCH_PATHS = (
../../ThirdParty/vorbis/lib,
../../ThirdParty/flac/lib,
);
PRODUCT_BUNDLE_IDENTIFIER = org.cogx.vorbis;
PRODUCT_NAME = VorbisPlugin;
SDKROOT = macosx;

View File

@ -157,10 +157,10 @@ static NSString *get_description_tag(const char *description, const char *tag, c
@"floatingPoint": @(NO),
@"channels": @(channels),
@"seekable": @(YES),
@"replayGainAlbumGain": rgAlbumGain,
@"replayGainAlbumPeak": rgAlbumPeak,
@"replayGainTrackGain": rgTrackGain,
@"replayGainTrackPeak": rgTrackPeak,
@"replaygain_album_gain": rgAlbumGain,
@"replaygain_album_peak": rgAlbumPeak,
@"replaygain_track_gain": rgTrackGain,
@"replaygain_track_peak": rgTrackPeak,
@"codec": codec,
@"endian": @"host",
@"encoding": @"lossy/lossless" };

View File

@ -51,6 +51,17 @@
}
}
- (NSArray *)coalesceArray:(NSArray *)input {
if(input == nil) return input;
NSMutableArray *array = [[NSMutableArray alloc] init];
for(NSString *string in input) {
[array addObject:[self coalesceString:string]];
}
return [NSArray arrayWithArray:array];
}
- (NSDictionary *)coalesceEntryInfo:(NSDictionary *)entryInfo {
if(entryInfo == nil) return entryInfo;
@ -63,6 +74,9 @@
} else if([obj isKindOfClass:[NSData class]]) {
NSData *dataObj = (NSData *)obj;
[ret setObject:[self coalesceArt:dataObj] forKey:key];
} else if([obj isKindOfClass:[NSArray class]]) {
NSArray *arrayObj = (NSArray *)obj;
[ret setObject:[self coalesceArray:arrayObj] forKey:key];
} else {
[ret setObject:obj forKey:key];
}

View File

@ -14,9 +14,6 @@
/* Class = "NSTextFieldCell"; title = "Length:"; ObjectID = "16"; */
"16.title" = "Length:";
/* Class = "NSTextFieldCell"; title = "Year:"; ObjectID = "18"; */
"18.title" = "Year:";
/* Class = "NSTextFieldCell"; title = "Genre:"; ObjectID = "20"; */
"20.title" = "Genre:";
@ -79,3 +76,9 @@
/* Class = "NSTextFieldCell"; title = "Codec:"; ObjectID = "QPg-Mb-Urn"; */
"QPg-Mb-Urn.title" = "Codec:";
/* Class = "NSTextFieldCell"; title = "Date:"; ObjectID = "17"; */
"18.title" = "Date:";
/* Class = "NSTextFieldCell"; title = "Comment:"; ObjectID = "cd3-Qt-hCm"; */
"Ule-N3-dKW.title" = "Comment:";

View File

@ -16,9 +16,6 @@
/* Class = "NSTextFieldCell"; title = "Length:"; ObjectID = "16"; */
"16.title" = "Duración:";
/* Class = "NSTextFieldCell"; title = "Year:"; ObjectID = "18"; */
"18.title" = "Año:";
/* Class = "NSTextFieldCell"; title = "Genre:"; ObjectID = "20"; */
"20.title" = "Género:";
@ -109,3 +106,8 @@
/* Class = "NSTextFieldCell"; title = "N/A"; ObjectID = "Yby-OU-cqP"; */
"Yby-OU-cqP.title" = "n/d";
/* Class = "NSTextFieldCell"; title = "Date:"; ObjectID = "17"; */
"18.title" = "Date:";
/* Class = "NSTextFieldCell"; title = "Comment:"; ObjectID = "cd3-Qt-hCm"; */
"Ule-N3-dKW.title" = "Comment:";