Implement lookup and construct the new path name
[armadillo.git] / src / tv_rename.go
1 //
2 // Armadillo File Manager
3 // Copyright (c) 2011, Robert Sesek <http://www.bluestatic.org>
4 //
5 // This program is free software: you can redistribute it and/or modify it under
6 // the terms of the GNU General Public License as published by the Free Software
7 // Foundation, either version 3 of the License, or any later version.
8 //
9
10 package tv_rename
11
12 import (
13 "bufio"
14 "fmt"
15 "http"
16 "net"
17 "os"
18 "path"
19 "regexp"
20 "strconv"
21 "strings"
22 "./paths"
23 )
24
25 // Takes a full file path and renames the last path component as if it were a
26 // TV episode. This performs the actual rename as well.
27 func RenameEpisode(inPath string) (*string, os.Error) {
28 // Make sure a path was given.
29 if len(inPath) < 1 {
30 return nil, os.NewError("Invalid path")
31 }
32 // Check that it's inside the jail.
33 var safePath *string = paths.Verify(inPath)
34 if safePath == nil {
35 return nil, os.NewError("Path is invalid or outside of jail")
36 }
37 // Make sure that the file exists.
38 _, err := os.Stat(*safePath)
39 if err != nil {
40 return nil, err
41 }
42
43 // Parse the filename into its components.
44 dirName, fileName := path.Split(*safePath)
45 info := parseEpisodeName(fileName)
46 if info == nil {
47 return nil, os.NewError("Could not parse file name")
48 }
49
50 // Create the URL and perform the lookup.
51 queryURL := buildURL(info)
52 response, err := performLookup(queryURL)
53 if err != nil {
54 return nil, err
55 }
56
57 // Parse the response into the fullEpisodeInfo struct.
58 fullInfo := parseResponse(response)
59
60 // Create the new path.
61 newName := fmt.Sprintf("%s - %dx%02d - %s", fullInfo.episode.showName,
62 fullInfo.episode.season, fullInfo.episode.episode, fullInfo.episodeName)
63 newName = strings.Replace(newName, "/", "_", -1)
64 newName += path.Ext(fileName)
65 newPath := path.Join(dirName, newName)
66
67 return &newPath, nil
68 }
69
70 type episodeInfo struct {
71 showName string
72 season int
73 episode int
74 }
75
76 type fullEpisodeInfo struct {
77 episode episodeInfo
78 episodeName string
79 }
80
81 // Parses the last path component into a the component structure.
82 func parseEpisodeName(name string) *episodeInfo {
83 regex := regexp.MustCompile("(.+)( |\\.)[sS]?([0-9]+)[xeXE]([0-9]+)")
84 matches := regex.FindAllStringSubmatch(name, -1)
85 if len(matches) < 1 || len(matches[0]) < 4 {
86 return nil
87 }
88
89 // Convert the season and episode numbers to integers.
90 season, episode := convertEpisode(matches[0][3], matches[0][4])
91 if season == 0 && season == episode {
92 return nil
93 }
94
95 // If the separator between the show title and episode is a period, then
96 // it's likely of the form "some.show.name.s03e06.720p.blah.mkv", so strip the
97 // periods in the title.
98 var showName string = matches[0][1]
99 if matches[0][2] == "." {
100 showName = strings.Replace(matches[0][1], ".", " ", -1)
101 }
102
103 return &episodeInfo {
104 showName,
105 season,
106 episode,
107 }
108 }
109
110 // Builds the URL to which we send a HTTP request to get the episode name.
111 func buildURL(info *episodeInfo) string {
112 return fmt.Sprintf("http://services.tvrage.com/tools/quickinfo.php?show=%s&ep=%dx%d",
113 http.URLEscape(info.showName), info.season, info.episode)
114 }
115
116 // Converts a season and episode to integers. If the return values are both 0,
117 // an error occurred.
118 func convertEpisode(season string, episode string) (int, int) {
119 seasonInt, err := strconv.Atoi(season)
120 if err != nil {
121 return 0, 0
122 }
123 episodeInt, err := strconv.Atoi(episode)
124 if err != nil {
125 return 0, 0
126 }
127 return seasonInt, episodeInt
128 }
129
130 // Performs the actual lookup and returns the HTTP response.
131 func performLookup(urlString string) (*http.Response, os.Error) {
132 url, err := http.ParseURL(urlString)
133 if err != nil {
134 return nil, err
135 }
136
137 // Open a TCP connection.
138 conn, err := net.Dial("tcp", "", url.Host + ":" + url.Scheme)
139 if err != nil {
140 return nil, err
141 }
142
143 // Perform the HTTP request.
144 client := http.NewClientConn(conn, nil)
145 var request http.Request
146 request.URL = url
147 request.Method = "GET"
148 request.UserAgent = "Armadillo File Manager"
149 err = client.Write(&request)
150 if err != nil {
151 return nil, err
152 }
153 return client.Read()
154 }
155
156 // Parses the HTTP response from performLookup().
157 func parseResponse(response *http.Response) *fullEpisodeInfo {
158 var err os.Error
159 var line string
160 var info fullEpisodeInfo
161
162 buf := bufio.NewReader(response.Body)
163 for ; err != os.EOF; line, err = buf.ReadString('\n') {
164 // An error ocurred while reading.
165 if err != nil {
166 return nil
167 }
168 var parts []string = strings.Split(line, "@", 2)
169 if len(parts) != 2 {
170 continue
171 }
172 switch parts[0] {
173 case "Show Name":
174 info.episode.showName = parts[1]
175 case "Episode Info":
176 // Split the line, which is of the form: |SxE^Name^AirDate|.
177 parts = strings.Split(parts[1], "^", 3)
178 info.episodeName = parts[1]
179 // Split the episode string.
180 episode := strings.Split(parts[0], "x", 2)
181 info.episode.season, info.episode.episode = convertEpisode(episode[0], episode[1])
182 if info.episode.season == 0 && info.episode.season == info.episode.episode {
183 return nil
184 }
185 }
186 }
187 return &info
188 }