Rename files within folders to folder names while retaining extensions - bash

I have a large repository of media files that follow torrent naming conventions- something unpleasant to read. At one point, I had properly named the folders that contain said files, but not want to dump all the .avi, .mkv, etc files into my main media directory using a bash script.
Overview:
Current directory tree:
Proper Movie Title/
->Proper.Movie.Title.2013.avi
->Proper.Movie.Title.2013.srt
Title 2/
->Title2[proper].mkv
Movie- Epilogue/
->MOVIE EPILOGUE .AVI
Media Movie/
->MEDIAMOVIE.CD1.mkv
->MEDIAMOVIE.CD2.mkv
.
.
.
Desired directory tree:
Proper Movie Title/
->Proper Movie Title.avi
->Proper Movie Title.srt
Title 2.mkv
Movie- Epilogue.avi
Media Movie/
->Media Movie.cd1.mkv
->Media Movie.cd2.mkv
Though this would be an ideal, my main wish is for the directories with only a single movie file within to have that file be renamed and moved into the parent directory.
My current approach is to use a double for loop in a .sh file, but I'm currently having a hard time keeping new bash knowledge in my head.
Help would be appreciated.
My current code (Just to get access to the internal movie files):
#!/bin/bash
FILES=./*
for f in $FILES
do
if [[ -d $f ]]; then
INFILES=$f/*
for file in $INFILES
do
echo "Processing >$file< folder..."
done
#cat $f
fi
done

Here's something simple:
find * -type f -maxdepth 1 | while read file
do
dirname="$(dirname "$file")"
new_name="${dirname##*/}"
file_ext=${file##*.}
if [ -n "$file_ext" -a -n "$dirname" -a -n "$new_name" ]
then
echo "mv '$file' '$dirname/$new_name.$file_ext'"
fi
done
The find * says to run find on all items in the current directory. The -type f says you only are interested in files, and -maxdepth 1 limits the depth of the search to the immediate directory.
The ${file##*.} is using a pattern match. The ## says the largest left hand match to *. which is basically pulling everything off to the file extension.
The file_dir="$(dirname "$file")" gets the directory name.
Note quotes everywhere! You have to be careful about white spaces.
By the way, I echo instead of doing the actual move. I can pipe the output to a file, examine that file and make sure everything looks okay, then run that file as a shell script.

Related

Shell script for finding (and deleting) video files if they came from a rar

My download program automatically unrars rar archives, which is all well and good as Sonarr and Radarr need that original video file to import. But now my download HDD fills up with all these video files I no longer need.
I've tried playing around with modifying existing scripts I have, but every step seems to take me further from the goal.
Here's what I have so far (that isnt working and I clearly dont know what im doing). My main problem is I can't get it to find the files correctly yet. This script jumps right to "no files found". So I'm doing the search wrong at the very least. Or I'm pretty sure I might need to completely rewrite from scratch using a different method I'm not aware of..
#!/bin/bash
# Find video files and if it came from a rar, remove it.
# If no directory is given, work in local dir
if [ "$1" = "" ]; then
DIR="."
else
DIR="$1"
fi
# Find all the MKV files in this dir and its subdirs
find "$DIR" -type f -name '*.mkv' | while read filename
do
# If video file and rar file exists, delete mkv.
for f in ...
do
if [[ -f "$DIR/*.mkv" ]] && [[ -f "$DIR/*.rar" ]]
then
# rm $filename
printf "[Dry run delete]: $filename\n"
else
printf "No files found\n"
exit 1
fi
done
Example of directory structure before and after. Note the file names are often different to the extracted file. And I want to leave other folders that don't have rars in them alone.
Before:
/folder/moviename/Movie.that.came.from.rar.2021.dvdrip.mkv
/folder/moviename/movie.rar
/folder/moviename/movie.r00
/folder/moviename/movie.r01
/folder/moviename2/Movie.that.lives.alone.2021.dvdrip.mkv
/folder/moviename2/Movie.2021.dvdrip.nfo
After
# (deleted the mkv only from the first folder)
/folder/moviename/movie.rar
/folder/moviename/movie.r00
/folder/moviename/movie.r01
# (this mkv survives)
/folder/moviename2/Movie.that.lives.alone.2021.dvdrip.mkv
/folder/moviename2/Movie.2021.dvdrip.nfo
TL:DR I would like a script to look recursively in my download drive for video files and rar files, and if it sees both in the same folder, delete the video file.
With GNU find, you can condense this to one command:
find "${1:-.}" -type f -name '*.rar' -execdir sh -c 'echo rm *.mkv' \;
${1:-.} says "use $1, or . if $1 is undefined or empty".
For each .rar file found, this starts a new shell in the directory of the file found (that's what -execdir sh -c '...' does) and runs echo rm *.mkv.
If the list of files to delete looks correct, you can actually delete them by dropping the echo:
find "${1:-.}" -type f -name '*.rar' -execdir sh -c 'rm *.mkv' \;
Two remarks, though:
-execdir rm *.mkv \; would be shorter, but then the glob might be expanded prematurely in case there are .mkv files in the current directory
if a directory contains a .rar file, but no .mkv, this will try to delete a file called literally *.mkv and cause an error message

Bash scanning for filenames containing keywords and move them

I'm looking to find a way to constantly scan a folder tree for new subfolders containing MKV/MP4 files. If that file contains a keyword and ends in MP4 or MKV, it'll be moved to a defined location matching that keyword. As a bonus, it would delete the folder and all it's leftover contents where the file resided previosly. The idea would be to have this run in the background and sort everything where it belongs and clean up after itself if possible.
example:
Media\anime\Timmy\Timmy_S1E1\Timmy_S1E1_720p.mkv #Found Keyword Timmy, allowed filetype
Move to destination:
Media\series\Timmy\
Delete subfolder:
Media\anime\Timmy\Timmy_S1E1\
I would either do separate scripts for each keyword, or, if possible, have the script match each keyword with a destination
#!/bin/bash
#!/bin/sh
#!/etc/shells/bin/bash
while true
do
shopt -s globstar
start_dir="//srv/MEDIA2/shows"
for name in "$start_dir"/**/*.*; do
# search the directory recursively
done
sleep 300
done
This could be done by:
creating a script that does what you want to do, once.
run the script from cron, at a certain interval. Say a couple minutes, or a couple hours, depends on the volume of files you receive.
no need for a continually running daemon.
Ex:
#!/bin/bash
start_dir="/start/directory"
if [[ ! -d "$start_dir" ]]
then
echo "ERROR: start_dir ($start_dir) not found."
exit 1
fi
target_dir="/target/directory"
if [[ ! -d "$target_dir" ]]
then
echo "ERROR: target_dir ($target_dir) not found."
exit 1
fi
# Move all MP4 and MKV files to the target directory
find "$start_dir" -type f \( -name "*keyword*.MP4" -o -name "*keyword*.MKV" \) -print0 | while read -r -d $'\0' file
do
# add any processing here...
filename=$(basename "$file")
echo "Moving $filename to $target_dir..."
mv "$file" "$target_dir/$filename"
done
# That being done, all that is left in start_dir can be deleted
find "$start_dir" -type d ! -path "$start_dir" -exec /bin/rm -fr {} \;
Details:
scanning for files is most efficient with the find command
the -print0 with read ... method is to ensure all valid filenames are processed, even if they include spaces or other "weird" characters.
the result of the above code is that each file that matches your keyword, with extensions MP4 or MKV will be processed once.
you can then use "$file" to access the file being processed in the current loop.
make sure you ALWAYS double quote $file, otherwise any weird filename will brake your code. Well you should always double quote your variables anyway.
more complex logic can be added for your specific needs. Ex. create the target directory if it does not exist. Create a different target directory depending on your keyword. etc.
to delete all sub-directories under $start_dir, I use find. Again this will process weird directory names.
One point, some will argue that it could all be done in 1 find command with -exec option. True, but IMHO the version with the while loop is easier to code, understand, debug, learn.
And this construct is good to have in your bash toolbox.
When you create a script, only one #! line is needed.
And I fixed the indentation in your question, much easier to read your code properly indented and formatted (see the edit help in the question editor).
Last point to discuss, lets say you have a LARGE number of directories and files to process, and it is possible that new files are added while the script is running. Ex. you are moving many MP4 files, and while it is doing it, new files are deposited in the directories. Then when you do the deletion you could potentially loose files.
If such a case is possible, you could add a check for new files just before you do the /bin/rm, it would help. To be absolutely certain, you could setup a script that processes 1 file, and have it triggered by inotify. But that is another ball game, more complicated and out of scope for this answer.

Sort text files in bash

I have a number of text files in the current directory. Each file contains data about a movie.The content of each file will be according to the following format:
Movie name
Storyline
Director name
Year of release
How can I organize the files according to the name of the directors by a shell script. Movies made by same director will get moved to a folder named after the director. How can I do this by writing shell script?
The rough solution will be
for file in *; do
director=$( some code here depending on requirements clarification )
mkdir -p "$director" # quotes are crucial!
# -p suppresses errors if dir already exists.
mv -v "$file" "$director"
done
Assuming that the filename of the movie is always the first line, and the Director name is always the next to last. You could do it something similar to this:
#!/bin/bash
MOVIES_DIR=/path/to/your/movies
for FILE in ${MOVIES_DIR}/*.txt; do
# Movie name will always be the first line
MOVIE=$(sed '1q;d' "$FILE")
# Director will always be the next to last line
DIRECTOR=$(tac "$FILE" | sed '2q;d')
# Make the director folder
mkdir -p "${MOVIES_DIR}/${DIRECTOR}"
# Find inside the `MOVIES_DIR` files with the movie name with any extension
# That is NOT .txt and moves them to the proper director folder.
find "${MOVIES_DIR}" -maxdepth 1 -name "${MOVIE}.*" -not -name '*.txt' -print0 |
while IFS= read -r -d '' NAME; do
FILENAME=$(printf '%s\n' "$NAME")
mv "$FILENAME" "${MOVIES_DIR}/${DIRECTOR}"
done
done
Edit:
It was my misunderstanding that you wanted to move movies with the same name in the text file into the appropriate director folder. I now see you are intending to move the text files themselves. I downloaded all the text files from the link you provided, and it successfully sorted all of them into the appropriate folder:
#!/bin/bash
MOVIES_DIR=/path/to/your/files
for FILE in ${MOVIES_DIR}/*.txt; do
# I thought the description was on one line, but it looks like it can span
# multiple. So get the next to last line instead, since year is only one line. I also changed this in the first script.
DIRECTOR=$(tac "$FILE" | sed '2q;d')
DIRECTOR_PATH="$MOVIES_DIR/$DIRECTOR"
mkdir -p "$DIRECTOR_PATH"
# Move the current text file to the Director folder.
mv "$FILE" "$DIRECTOR_PATH"
done
The first script should sort all movies into the director folders, assuming that the text files are in the same directory as the movies.
The second script should sort all text files into the appropriate director folders.

Bash. Using find and a for loop. Can't find how to mv file into the directory that was found. Just moves to root directory given.

Current code.
Right now I have a test format set up. Here is what I have.
files=`find C:/PATH/TO/DIRECTORY/2014-05-08 -name *.txt`
(I will be running this from C:/PATH/TO/DIRECTORY. Just right now I'm only testing on todays directory so I don't change everything in /DIRECTORY.) The .sh script is being run from /DIRECTORY therefore it is it's root.) In the implemented version find will look like this:
files=`find C:/PATH/TO/DIRECTORY -name *.txt`
for file in $files; do
#code to manipulate filename
mv $file $newFilename
The problem that I am getting is that this renames the old file name into the new file name, but the new files are in /DIRECTORY when I want them to be in the directory that the program is currently looping through. Which in this case is /DIRECTORY/2014-05-08.
I was just going to make a variable for the current directory of $file, but couldn't figure out how and use:
mv $file /CWD/$newFilename
but I was unable to find a way to assign CWD the directory that $file was in. This would probably be the easiest way if it's possible.
This would be a better way to code that:
find dir -name \*.txt |
while IFS= read -r file; do
dir=$(dirname "$file")
file=$(basename "$file")
newfile=...whatever...
mv "$dir/$file" "$dir/$newfile"
done
Using a for loop over the results of find will iterate over the words in the result, not necessarily the lines -- if there's a filename with a space in it, the for loop won't get the right filename.
The while IFS= read -r line technique I demonstrate is the safest way to iterate over the lines of the result of a command.
There are other issues in my code that I don't think are strictly relevant here.

Get directories without specific filetype cygwin/bash

I am trying to organise my music collection's album art, for XBMC etc. and my own personal OCD based reasons. To do this, I want to loop through each folder in my music collection, (there are also files in music hence $(find . -type d). I check each folder for the existence of a jpeg. If it exists, it is copied to be called cover.jpg. If no jpeg is found, the name of the folder should be added to a list, which I'll then search google for myself. The code I'm using is below.
#!/bin/bash
IFS=$'\n'
touch missing
cd /cygdrive/d/music
for i in $(find . -type d)
do cd $i
if [ -e '*.jpg' ]
then
for j in *.jpg
do cp $j 'cover.jpg'
done
else
cd ..
$i >> missing
fi
done
The Problems
cd $i always fails, as the directory does not exist.
$i >> missing always fails, with a permission is denied error, however ls -l missing gives -rwxrwxrwx+ 1 Username None 0 Sep 13 16:45 missing
Update:
After trying several iterations to get the script to work, I had to give up and simply use this Album Art Downloader, which in hindsight was the obvious thing to do. I'm marking the answer with a revised script as accepted as it got me closer to the solution I know is out there, somewhere.
1) cd $i always fails, as the directory does not exist.
since the result of find . -type -d is relative to your where you initial working directory, you'll need to cd back to the base directory before doing the nextcd $1. A quick way to return to the previous directory is cd -.
2) $i >> missing always fails, with a permission is denied error,
Two problems here. First of all, by doing $i >> missing you're actually trying to execute $i as a command. If you want the filename appended to the file, you'll need to use echo $i >> missing.
Secondly, you created missing in your initial working directory, and so you're actually dealing with a different missing file once you've cd'ed somewhere else. To refer to the same file, use an absolute path.
Try this: (I tried to keep the script as close to what you have as possible and commented on lines that I changed. I've also quoted all paths to better handle filenames with spaces)
#!/bin/bash
IFS=$'\n'
MISSING="$(pwd)/missing" # store absolute path
> "$MISSING". # create new or empty existing
cd /cygdrive/d/music
for i in $(find . -type d)
do
cd "$i"
if [ -e *.jpg ] # wildcard not expanded if quoted
then
for j in *.jpg
do cp "$j" 'cover.jpg'
done
else
echo "$i" >> "$MISSING" # use $MISSING
fi
cd - # go back to previous directory
done
p.s. Just in case you haven't noticed, do be aware that if you have more than one *.jpg file in a directory, your script will essentially repeatedly overwrite cover.jpg with the last one ending up as the actual version. Also, you're likely to see the error "cp: 'cover.jpg' and 'cover.jpg' are the same file" if you have more than one jpg file and when cover.jpg already exists.
cd $i will fail for a file like "Artist Name - Song Name.mp3". The shell will break it into Artist Name etc. on whitespace and it will break. You could print $i before doing the cd to debug the problem.
$i >> missing will fail because you're trying to execute $i as a script and output the contents into missing. If you want to put the file name, you should use something like echo "$i" >> missing.
I recommend you use a real scripting language like Perl or Python rather than do this in shell. Files with spaces in the name are a real pain.

Resources