summaryrefslogtreecommitdiff
path: root/localsonganizer
blob: 68361181c060254294b17d74e23cac95f58ee6b5 (plain)
  1. #!/bin/bash
  2. #
  3. # songanizer - script to organize ogg and mp3 files
  4. # Copyright (c) 2002-2004 Patrick Ohnewein.
  5. # All rights reserved.
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program; if not, write to the Free Software
  19. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  20. #
  21. #
  22. # Description:
  23. #
  24. # Script to organize a directory containing ogg and mp3 files.
  25. #
  26. # The biggest problem for me, during my efforts to organize music files, was
  27. # the choice of the directory structure. Should the directory structure reflect
  28. # the author or the genre or may be the album? I ended up with the conclusion,
  29. # that no ideal directory structure exists. So I wanted different virtual
  30. # directory structures to the same data. Thanks to the symbolic links
  31. # capability of the file systems I use (ext2, ext3, ...) this dream has become
  32. # reality!
  33. #
  34. # The script gets a list of _data directories, in which the real ogg and mp3
  35. # files reside. The script has to read informations like author, album, genre,
  36. # ... from these files and create parallel directory structures, which
  37. # just contain symbolic links to the real files.
  38. #
  39. # The goal is to create virtual directory structures, which give different
  40. # views of the data, but without having redundant copies of the files
  41. # themselves.
  42. #
  43. # dependences:
  44. # - It's a bash script and therefore needs a running bash.
  45. # - Uses getopt (enhanced) to extract options from the command line arguments.
  46. # - Uses gettext (gettext.sh) for i18n support.
  47. # - The link structures are created using links on the file system and
  48. # therefore the file system must support them (i.e. ext2, ext3, ...)
  49. # - To read the mp3 tags the mp3info tool gets used.
  50. # Get it at http://ibiblio.org/mp3info/
  51. #
  52. # How is the directory structure organized?
  53. # In the base directory live the following directrories:
  54. # _data* all directories, with a name starting with _data, contain the
  55. # real data (all links will point to its content). These can also
  56. # be symbolic links to directories on other devices.
  57. # _artist contains the link structure on the basis of the artist tag
  58. # _genre contains the link structure on the basis of the genre tag
  59. # _initial contains the link structure on the basis of the initials
  60. # ... and more, depending on the switchs passed to the script
  61. #
  62. #
  63. # @version 0.8.20060507 - Make all options singular (fixing broken artist)
  64. # - Major reorganizing to separate targets from tags.
  65. # - Add new funtions: enableTargets, resolveTag,
  66. # resolveTarget
  67. # - Add new targets: decade, yeartree, decadetree,
  68. # genretree, artisttree, alltree
  69. # - Drop target: comments
  70. # - Fix bailing out if unable to create temp dir
  71. # - Rephrase help text
  72. # - Use safe tempdir
  73. # @version 0.8.20060211 - Major reorganizing to loop only once (not per-tag)
  74. # @version 0.8, 2005-11-02 - replaced ls with find [savannah bug #4932
  75. # task #2546]
  76. # - there can be multiple levels of sub-directories
  77. # inside every data directory [savannah bug #4933
  78. # task #2547]
  79. # - symbolic links point to individual files instead
  80. # of directories
  81. # - added the --all switch
  82. # - changed the creation algo, so it will first create
  83. # the structure in a tmp directory, and only after
  84. # completion of this heavy task, in a short
  85. # operation the structure is moved to its desired
  86. # location
  87. # @version 0.7, 2004-02-01 - added gettext support for i18n
  88. # @version 0.6, 2004-01-24 - added long options support
  89. # @version 0.5, 2003-08-22 - fixed problem with same album in more than one
  90. # data directory [savannah bug #4877]
  91. # @version 0.4, 2003-08-17 - added support for multiple data directories
  92. # @version 0.3, 2002-12-28 - translated all texts into english
  93. # @version 0.2, 2002-12-10 - created generic function organizeOnBaseOfTag()
  94. # @version 0.1, 2002-11-01 - started the project
  95. # @author Patrick Ohnewein (@lugbz.org)
  96. #
  97. # ToDo:
  98. # - add support for ID3v2 Tags - probably should go into mp3info
  99. #
  100. VERSION="0.8"
  101. # use gettext for internationalization
  102. . gettext.sh
  103. TEXTDOMAIN=songanizer
  104. export TEXTDOMAIN
  105. TEXTDOMAINDIR=$(dirname $0)/../share/locale
  106. export TEXTDOMAINDIR
  107. program_name=$(basename $0)
  108. # definition of the used error codes
  109. EX_OK=0
  110. EX_USAGE=64
  111. EX_SOFTWARE=70
  112. EX_CONFIG=78
  113. if [ -f sysexits ]; then
  114. # If an external sysexits file exists, we source it. This allows the
  115. # external overwriting of the error codes.
  116. source sysexits
  117. fi
  118. ifs="$IFS"
  119. newline='
  120. '
  121. # variables
  122. verbose=0
  123. alltargets="artist artisttree genre genretree year yeartree decade decadetree album"
  124. alltreetargets="artisttree genretree yeartree decadetree"
  125. alltags="artist genre year decade album"
  126. defaulttargets=""
  127. # Prints a version message.
  128. print_version ()
  129. {
  130. eval_gettext "songanizer, version ${VERSION}"
  131. echo
  132. # do not translate copyright notice.
  133. echo "Copyright (C) 2002-2004 Patrick Ohnewein"
  134. }
  135. # Prints a help message, explaining all the available options.
  136. print_help ()
  137. {
  138. gettext "Organizes files in one or more virtual directory structures."
  139. echo
  140. gettext "Options:"
  141. echo
  142. gettext " -A|--artist Group songs by artist"
  143. echo
  144. gettext " --artisttree Group songs by artist and album"
  145. echo
  146. gettext " -G|--genre Group songs by genre"
  147. echo
  148. gettext " --genretree Group songs by genre, artist and album"
  149. echo
  150. gettext " -Y|--year Group songs by year"
  151. echo
  152. gettext " --yeartree Group songs by year, artist and album"
  153. echo
  154. gettext " -D|--decade Group songs by decade"
  155. echo
  156. gettext " --decadetree Group songs by decade, artist and album"
  157. echo
  158. gettext " -L|--album Group songs by album"
  159. echo
  160. gettext " --all Group songs by any and all of the above"
  161. echo
  162. gettext " --alltree Group songs by any and all of above trees"
  163. echo
  164. gettext " -h|--help Print this help screen"
  165. echo
  166. gettext " -v|--verbose Activate verbose mode"
  167. echo
  168. gettext " --version Print version information"
  169. echo
  170. }
  171. # Prints a usage message, explaining how the script has to be called.
  172. print_usage ()
  173. {
  174. eval_gettext "Usage: $program_name [options] basedir"
  175. echo
  176. print_help
  177. }
  178. enableTargets () {
  179. for target in $@; do
  180. target_is_wanted=""
  181. for possibletarget in $alltargets; do
  182. if [ "$possibletarget" = "$target" ]; then
  183. target_is_wanted="1"
  184. fi
  185. done
  186. if [ -n "$target_is_wanted" ]; then
  187. for existingtarget in $wantedtargets; do
  188. if [ "$existingtarget" = "$target" ]; then
  189. target_is_wanted=""
  190. fi
  191. done
  192. if [ -n "$target_is_wanted" ]; then
  193. wantedtargets="$wantedtargets $target"
  194. fi
  195. else
  196. eval_gettext "Error ${program_name}: internal error: unknown target \"$target\""
  197. echo
  198. exit ${EX_SOFTWARE}
  199. fi
  200. done
  201. }
  202. TEMP=$(getopt -o :AGYDLhv --long all,alltree,artist,artisttree,genre,genretree,year,decade,decadetree,album,help,verbose,version -n "$program_name" -- "$@") #"
  203. if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
  204. eval set -- "$TEMP"
  205. wantedtargets="$defaulttargets"
  206. while true ; do
  207. case "$1" in
  208. -A|--artist) enableTargets artist; shift;;
  209. --artisttree) enableTargets artisttree; shift;;
  210. -G|--genre) enableTargets genre; shift;;
  211. --genretree) enableTargets genretree; shift;;
  212. -Y|--year) enableTargets year; shift;;
  213. --yeartree) enableTargets yeartree; shift;;
  214. -D|--decade) enableTargets decade; shift;;
  215. --decadetree) enableTargets decadetree; shift;;
  216. -L|--album) enableTargets album; shift;;
  217. --all) enableTargets "$alltargets"; shift;;
  218. --alltree) enableTargets "$alltreetargets"; shift;;
  219. -h|--help) print_help ; exit ${EX_OK} ; shift;;
  220. -v|--verbose) verbose=1; shift;;
  221. --version) print_version ; exit ${EX_OK} ; shift;;
  222. --) shift ; break ;;
  223. *) gettext "Unimplemented option choosen. Use -h to visualize a help screen."; echo; exit ${EX_USAGE} ;;
  224. esac
  225. done
  226. if [ $# -eq 0 ]; then
  227. print_usage
  228. exit ${EX_USAGE}
  229. fi
  230. tempdir="`mktemp -tdq \"songanizer.XXXXXXXX\"`"
  231. if [ ! $? -eq 0 ]; then
  232. eval_gettext "Error ${program_name}: Could not create temporary config file. exiting"
  233. echo
  234. exit ${EX_CONFIG}
  235. fi
  236. basedir=${1}
  237. if [ "${basedir:0:1}" != "/" ]; then
  238. basedir="`pwd`/${basedir}"
  239. fi
  240. if [ ! -d ${basedir} ]; then
  241. eval_gettext "Error ${program_name}: base directory ${basedir} doesn't exist"
  242. echo
  243. exit ${EX_CONFIG}
  244. fi
  245. datadir="${basedir}/_data"
  246. datadirs="${datadir}*"
  247. for datadir_elem in ${datadirs}; do
  248. if [ ! -d ${datadir_elem} ]; then
  249. eval_gettext "Error ${program_name}: invalid or no data directory ${datadir_elem}"
  250. echo
  251. exit ${EX_CONFIG}
  252. elif [ $verbose -ne 0 ]; then
  253. echo "Detected data directory: ${datadir_elem}"
  254. fi
  255. done
  256. initTempTargetDirs () {
  257. # pessimistic aproach
  258. EXIT_CODE=${EX_SOFTWARE}
  259. IFS="$newline"
  260. for targetname in $@; do IFS="$ifs";
  261. temptargetdir="${tempdir}/_${targetname}"
  262. if [ ! -d "${temptargetdir}" ]; then
  263. mkdir "${temptargetdir}"
  264. else
  265. # remove all sub directories
  266. if [ $verbose -ne 0 ]; then echo "rm -rf ${temptargetdir}/*"; fi
  267. rm -rf "${temptargetdir}/"*
  268. fi
  269. if [ ! -d "${temptargetdir}" ]; then
  270. eval_gettext "Error ${program_name}: Couldn't create directory ${temptargetdir}"
  271. echo
  272. exit ${EX_SOFTWARE}
  273. fi
  274. done
  275. IFS="$ifs"
  276. }
  277. resolveTag () {
  278. # pessimistic aproach
  279. EXIT_CODE=${EX_SOFTWARE}
  280. tagname="$1"; shift
  281. tagdump="$1"; shift
  282. case "$tagname" in
  283. genre) tagpattern="^GENRE";;
  284. artist) tagpattern="^ARTIST";;
  285. album) tagpattern="^ALBUM";;
  286. year) tagpattern="^DATE";;
  287. decade) tagpattern="^DATE";;
  288. esac
  289. tag="`echo \"$tagdump\" | grep \"$tagpattern\" | awk -F= '{ print $2 }'`"
  290. case "$tagname" in
  291. decade) if [ -n "$tag" ]; then tag="`perl -e \"print int($tag / 10) . 0 if $tag\"`"; fi
  292. esac
  293. # exchange '/', which is invalid for directory
  294. # names with the neutral character '-'
  295. tag="${tag//\//-}"
  296. # if the filetag begins with two dots '..'
  297. # than we exchange them with a '-', to avoid
  298. # confusing the file operations
  299. tag="${tag//#../-}"
  300. if [ -z "${tag}" ]; then
  301. tag="UNKNOWN $tagname"
  302. fi
  303. echo "$tag"
  304. }
  305. resolveTarget () {
  306. # pessimistic aproach
  307. EXIT_CODE=${EX_SOFTWARE}
  308. targetname="$1"; shift
  309. tagdump="$1"; shift
  310. for tag in $alltags; do
  311. if [ "$tag" = "$targetname" ]; then
  312. target="`resolveTag \"$targetname\" \"$tagdump\"`"
  313. fi
  314. done
  315. case "$targetname" in
  316. artisttree) target="`resolveTag artist \"$tagdump\"`/`resolveTag album \"$tagdump\"`";;
  317. genretree) target="`resolveTag genre \"$tagdump\"`/`resolveTag artist \"$tagdump\"`/`resolveTag album \"$tagdump\"`";;
  318. yeartree) target="`resolveTag year \"$tagdump\"`/`resolveTag artist \"$tagdump\"`/`resolveTag album \"$tagdump\"`";;
  319. decadetree) target="`resolveTag decade \"$tagdump\"`/`resolveTag artist \"$tagdump\"`/`resolveTag album \"$tagdump\"`";;
  320. esac
  321. echo "$target"
  322. }
  323. symlinksToFile () {
  324. # pessimistic aproach
  325. EXIT_CODE=${EX_SOFTWARE}
  326. file="$1"; shift
  327. filetype="$1"; shift
  328. filename="`basename "$file"`"
  329. case "$filetype" in
  330. ogg)
  331. tagdump="`vorbiscomment -l "${file}"`"
  332. ;;
  333. mp3)
  334. tagdump="`mp3info -p 'GENRE=%g\nARTIST=%a\nALBUM=%n\nDATE=%y\n' \"${file}\" 2>/dev/null`"
  335. ;;
  336. *)
  337. echo "Error ${program_name}: unsupported filetype"
  338. echo
  339. exit ${EX_SOFTWARE}
  340. ;;
  341. esac
  342. IFS="$newline"
  343. for targetname in $@; do IFS="$ifs";
  344. target="`resolveTarget "$targetname" "$tagdump"`"
  345. targettempdir="${tempdir}/_${targetname}"
  346. destdir="${targettempdir}/${target}"
  347. if [ ! -d "${destdir}" ]; then
  348. if [ $verbose -ne 0 ]; then eval_gettext "Creating ${destdir} ..."; echo; fi
  349. mkdir -p "${destdir}"
  350. fi
  351. if [ ! -d "${destdir}" ]; then
  352. eval_gettext "Warning ${program_name}: Couldn't create directory ${destdir}"
  353. echo
  354. else
  355. destfile="${destdir}/${filename}"
  356. if [ -e "${destfile}" ]; then
  357. eval_gettext "Warning ${program_name}: Link already exists. File '${filename}' is probably contained in more than one data directory!"
  358. echo
  359. else
  360. if [ $verbose -ne 0 ]; then eval_gettext "Linking ${file} to ${destfile}"; echo; fi
  361. ln -s "${file}" "${destfile}"
  362. fi
  363. fi
  364. done
  365. IFS="$ifs"
  366. }
  367. moveSymlinkDirs () {
  368. # pessimistic aproach
  369. EXIT_CODE=${EX_SOFTWARE}
  370. IFS="$newline"
  371. for targetname in $@; do IFS="$ifs";
  372. targettempdir="${tempdir}/_${targetname}"
  373. eval_gettext "Moving ${targettempdir} to ${basedir} ..."
  374. echo
  375. mv ${targettempdir} ${basedir} 2>/dev/null
  376. if [ ! -d "${basedir}/_${targetname}" ]; then
  377. eval_gettext "Error $program_name: Couldn't move ${targettempdir} to ${basedir}!"
  378. echo
  379. exit ${EX_SOFTWARE}
  380. fi
  381. done
  382. IFS="$ifs"
  383. }
  384. initTempTargetDirs $wantedtargets
  385. find ${datadirs} \( -iname "*.mp3" -or -iname "*.mpeg" \) -and -type f -print | (while read file; do symlinksToFile "$file" mp3 $wantedtargets; done)
  386. find ${datadirs} -iname "*.ogg" -and -type f -print | (while read file; do symlinksToFile "$file" ogg $wantedtargets; done)
  387. moveSymlinkDirs $wantedtargets