write.apetag.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <?php
  2. /////////////////////////////////////////////////////////////////
  3. /// getID3() by James Heinrich <info@getid3.org> //
  4. // available at https://github.com/JamesHeinrich/getID3 //
  5. // or https://www.getid3.org //
  6. // or http://getid3.sourceforge.net //
  7. // see readme.txt for more details //
  8. /////////////////////////////////////////////////////////////////
  9. // //
  10. // write.apetag.php //
  11. // module for writing APE tags //
  12. // dependencies: module.tag.apetag.php //
  13. // ///
  14. /////////////////////////////////////////////////////////////////
  15. if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
  16. exit;
  17. }
  18. getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, true);
  19. class getid3_write_apetag
  20. {
  21. /**
  22. * @var string
  23. */
  24. public $filename;
  25. /**
  26. * @var array
  27. */
  28. public $tag_data;
  29. /**
  30. * ReplayGain / MP3gain tags will be copied from old tag even if not passed in data.
  31. *
  32. * @var bool
  33. */
  34. public $always_preserve_replaygain = true;
  35. /**
  36. * Any non-critical errors will be stored here.
  37. *
  38. * @var array
  39. */
  40. public $warnings = array();
  41. /**
  42. * Any critical errors will be stored here.
  43. *
  44. * @var array
  45. */
  46. public $errors = array();
  47. public function __construct() {
  48. }
  49. /**
  50. * @return bool
  51. */
  52. public function WriteAPEtag() {
  53. // NOTE: All data passed to this function must be UTF-8 format
  54. $getID3 = new getID3;
  55. $ThisFileInfo = $getID3->analyze($this->filename);
  56. if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['lyrics3']['tag_offset_end'])) {
  57. if ($ThisFileInfo['ape']['tag_offset_start'] >= $ThisFileInfo['lyrics3']['tag_offset_end']) {
  58. // Current APE tag between Lyrics3 and ID3v1/EOF
  59. // This break Lyrics3 functionality
  60. if (!$this->DeleteAPEtag()) {
  61. return false;
  62. }
  63. $ThisFileInfo = $getID3->analyze($this->filename);
  64. }
  65. }
  66. if ($this->always_preserve_replaygain) {
  67. $ReplayGainTagsToPreserve = array('mp3gain_minmax', 'mp3gain_album_minmax', 'mp3gain_undo', 'replaygain_track_peak', 'replaygain_track_gain', 'replaygain_album_peak', 'replaygain_album_gain');
  68. foreach ($ReplayGainTagsToPreserve as $rg_key) {
  69. if (isset($ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0]) && !isset($this->tag_data[strtoupper($rg_key)][0])) {
  70. $this->tag_data[strtoupper($rg_key)][0] = $ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0];
  71. }
  72. }
  73. }
  74. if ($APEtag = $this->GenerateAPEtag()) {
  75. if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) {
  76. $oldignoreuserabort = ignore_user_abort(true);
  77. flock($fp, LOCK_EX);
  78. $PostAPEdataOffset = $ThisFileInfo['avdataend'];
  79. if (isset($ThisFileInfo['ape']['tag_offset_end'])) {
  80. $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['ape']['tag_offset_end']);
  81. }
  82. if (isset($ThisFileInfo['lyrics3']['tag_offset_start'])) {
  83. $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['lyrics3']['tag_offset_start']);
  84. }
  85. fseek($fp, $PostAPEdataOffset);
  86. $PostAPEdata = '';
  87. if ($ThisFileInfo['filesize'] > $PostAPEdataOffset) {
  88. $PostAPEdata = fread($fp, $ThisFileInfo['filesize'] - $PostAPEdataOffset);
  89. }
  90. fseek($fp, $PostAPEdataOffset);
  91. if (isset($ThisFileInfo['ape']['tag_offset_start'])) {
  92. fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
  93. }
  94. ftruncate($fp, ftell($fp));
  95. fwrite($fp, $APEtag, strlen($APEtag));
  96. if (!empty($PostAPEdata)) {
  97. fwrite($fp, $PostAPEdata, strlen($PostAPEdata));
  98. }
  99. flock($fp, LOCK_UN);
  100. fclose($fp);
  101. ignore_user_abort($oldignoreuserabort);
  102. return true;
  103. }
  104. }
  105. return false;
  106. }
  107. /**
  108. * @return bool
  109. */
  110. public function DeleteAPEtag() {
  111. $getID3 = new getID3;
  112. $ThisFileInfo = $getID3->analyze($this->filename);
  113. if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['ape']['tag_offset_end'])) {
  114. if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) {
  115. flock($fp, LOCK_EX);
  116. $oldignoreuserabort = ignore_user_abort(true);
  117. fseek($fp, $ThisFileInfo['ape']['tag_offset_end']);
  118. $DataAfterAPE = '';
  119. if ($ThisFileInfo['filesize'] > $ThisFileInfo['ape']['tag_offset_end']) {
  120. $DataAfterAPE = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['ape']['tag_offset_end']);
  121. }
  122. ftruncate($fp, $ThisFileInfo['ape']['tag_offset_start']);
  123. fseek($fp, $ThisFileInfo['ape']['tag_offset_start']);
  124. if (!empty($DataAfterAPE)) {
  125. fwrite($fp, $DataAfterAPE, strlen($DataAfterAPE));
  126. }
  127. flock($fp, LOCK_UN);
  128. fclose($fp);
  129. ignore_user_abort($oldignoreuserabort);
  130. return true;
  131. }
  132. return false;
  133. }
  134. return true;
  135. }
  136. /**
  137. * @return string|false
  138. */
  139. public function GenerateAPEtag() {
  140. // NOTE: All data passed to this function must be UTF-8 format
  141. $items = array();
  142. if (!is_array($this->tag_data)) {
  143. return false;
  144. }
  145. foreach ($this->tag_data as $key => $arrayofvalues) {
  146. if (!is_array($arrayofvalues)) {
  147. return false;
  148. }
  149. $valuestring = '';
  150. foreach ($arrayofvalues as $value) {
  151. $valuestring .= str_replace("\x00", '', $value)."\x00";
  152. }
  153. $valuestring = rtrim($valuestring, "\x00");
  154. // Length of the assigned value in bytes
  155. $tagitem = getid3_lib::LittleEndian2String(strlen($valuestring), 4);
  156. //$tagitem .= $this->GenerateAPEtagFlags(true, true, false, 0, false);
  157. $tagitem .= "\x00\x00\x00\x00";
  158. $tagitem .= $this->CleanAPEtagItemKey($key)."\x00";
  159. $tagitem .= $valuestring;
  160. $items[] = $tagitem;
  161. }
  162. return $this->GenerateAPEtagHeaderFooter($items, true).implode('', $items).$this->GenerateAPEtagHeaderFooter($items, false);
  163. }
  164. /**
  165. * @param array $items
  166. * @param bool $isheader
  167. *
  168. * @return string
  169. */
  170. public function GenerateAPEtagHeaderFooter(&$items, $isheader=false) {
  171. $tagdatalength = 0;
  172. foreach ($items as $itemdata) {
  173. $tagdatalength += strlen($itemdata);
  174. }
  175. $APEheader = 'APETAGEX';
  176. $APEheader .= getid3_lib::LittleEndian2String(2000, 4);
  177. $APEheader .= getid3_lib::LittleEndian2String(32 + $tagdatalength, 4);
  178. $APEheader .= getid3_lib::LittleEndian2String(count($items), 4);
  179. $APEheader .= $this->GenerateAPEtagFlags(true, true, $isheader, 0, false);
  180. $APEheader .= str_repeat("\x00", 8);
  181. return $APEheader;
  182. }
  183. /**
  184. * @param bool $header
  185. * @param bool $footer
  186. * @param bool $isheader
  187. * @param int $encodingid
  188. * @param bool $readonly
  189. *
  190. * @return string
  191. */
  192. public function GenerateAPEtagFlags($header=true, $footer=true, $isheader=false, $encodingid=0, $readonly=false) {
  193. $APEtagFlags = array_fill(0, 4, 0);
  194. if ($header) {
  195. $APEtagFlags[0] |= 0x80; // Tag contains a header
  196. }
  197. if (!$footer) {
  198. $APEtagFlags[0] |= 0x40; // Tag contains no footer
  199. }
  200. if ($isheader) {
  201. $APEtagFlags[0] |= 0x20; // This is the header, not the footer
  202. }
  203. // 0: Item contains text information coded in UTF-8
  204. // 1: Item contains binary information °)
  205. // 2: Item is a locator of external stored information °°)
  206. // 3: reserved
  207. $APEtagFlags[3] |= ($encodingid << 1);
  208. if ($readonly) {
  209. $APEtagFlags[3] |= 0x01; // Tag or Item is Read Only
  210. }
  211. return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]);
  212. }
  213. /**
  214. * @param string $itemkey
  215. *
  216. * @return string
  217. */
  218. public function CleanAPEtagItemKey($itemkey) {
  219. $itemkey = preg_replace("#[^\x20-\x7E]#i", '', $itemkey);
  220. // http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html
  221. switch (strtoupper($itemkey)) {
  222. case 'EAN/UPC':
  223. case 'ISBN':
  224. case 'LC':
  225. case 'ISRC':
  226. $itemkey = strtoupper($itemkey);
  227. break;
  228. default:
  229. $itemkey = ucwords($itemkey);
  230. break;
  231. }
  232. return $itemkey;
  233. }
  234. }