Update package names and import paths
[armadillo.git] / server / tv_rename.go
1 //
2 // Armadillo File Manager
3 // Copyright (c) 2011-2012, 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 server
11
12 import (
13 "bufio"
14 "fmt"
15 "http"
16 "net"
17 "os"
18 "path"
19 "regexp"
20 "strconv"
21 "strings"
22 "url"
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 // Parse the filename into its components.
29 dirName, fileName := path.Split(inPath)
30 info := parseEpisodeName(fileName)
31 if info == nil {
32 return nil, os.NewError("Could not parse file name")
33 }
34
35 // Create the URL and perform the lookup.
36 queryURL := buildURL(info)
37 response, err := performLookup(queryURL)
38 if err != nil {
39 return nil, err
40 }
41
42 // Parse the response into the fullEpisodeInfo struct.
43 fullInfo := parseResponse(response)
44
45 // Create the new path.
46 newName := fmt.Sprintf("%s - %dx%02d - %s", fullInfo.episode.showName,
47 fullInfo.episode.season, fullInfo.episode.episode, fullInfo.episodeName)
48 newName = strings.Replace(newName, "/", "_", -1)
49 newName += path.Ext(fileName)
50 newPath := path.Join(dirName, newName)
51
52 return &newPath, nil
53 }
54
55 type episodeInfo struct {
56 showName string
57 season int
58 episode int
59 }
60
61 type fullEpisodeInfo struct {
62 episode episodeInfo
63 episodeName string
64 }
65
66 // Parses the last path component into a the component structure.
67 func parseEpisodeName(name string) *episodeInfo {
68 regex := regexp.MustCompile("(.+)( |\\.)[sS]?([0-9]+)[xeXE]([0-9]+)")
69 matches := regex.FindAllStringSubmatch(name, -1)
70 if len(matches) < 1 || len(matches[0]) < 4 {
71 return nil
72 }
73
74 // Convert the season and episode numbers to integers.
75 season, episode := convertEpisode(matches[0][3], matches[0][4])
76 if season == 0 && season == episode {
77 return nil
78 }
79
80 // If the separator between the show title and episode is a period, then
81 // it's likely of the form "some.show.name.s03e06.720p.blah.mkv", so strip the
82 // periods in the title.
83 var showName string = matches[0][1]
84 if matches[0][2] == "." {
85 showName = strings.Replace(matches[0][1], ".", " ", -1)
86 }
87
88 return &episodeInfo{
89 showName,
90 season,
91 episode,
92 }
93 }
94
95 // Builds the URL to which we send a HTTP request to get the episode name.
96 func buildURL(info *episodeInfo) string {
97 return fmt.Sprintf("http://services.tvrage.com/tools/quickinfo.php?show=%s&ep=%dx%d",
98 url.QueryEscape(info.showName), info.season, info.episode)
99 }
100
101 // Converts a season and episode to integers. If the return values are both 0,
102 // an error occurred.
103 func convertEpisode(season string, episode string) (int, int) {
104 seasonInt, err := strconv.Atoi(season)
105 if err != nil {
106 return 0, 0
107 }
108 episodeInt, err := strconv.Atoi(episode)
109 if err != nil {
110 return 0, 0
111 }
112 return seasonInt, episodeInt
113 }
114
115 // Performs the actual lookup and returns the HTTP response.
116 func performLookup(urlString string) (*http.Response, os.Error) {
117 url_, err := url.Parse(urlString)
118 if err != nil {
119 return nil, err
120 }
121
122 // Open a TCP connection.
123 conn, err := net.Dial("tcp", url_.Host+":"+url_.Scheme)
124 if err != nil {
125 return nil, err
126 }
127
128 // Perform the HTTP request.
129 client := http.NewClientConn(conn, nil)
130 request, err := http.NewRequest("GET", urlString, nil)
131 if err != nil {
132 return nil, err
133 }
134 request.Header.Set("User-Agent", "Armadillo File Manager")
135 err = client.Write(request)
136 if err != nil {
137 return nil, err
138 }
139 return client.Read(request)
140 }
141
142 // Parses the HTTP response from performLookup().
143 func parseResponse(response *http.Response) *fullEpisodeInfo {
144 var err os.Error
145 var line string
146 var info fullEpisodeInfo
147
148 buf := bufio.NewReader(response.Body)
149 for ; err != os.EOF; line, err = buf.ReadString('\n') {
150 // An error ocurred while reading.
151 if err != nil {
152 return nil
153 }
154 var parts []string = strings.SplitN(line, "@", 2)
155 if len(parts) != 2 {
156 continue
157 }
158 switch parts[0] {
159 case "Show Name":
160 info.episode.showName = strings.TrimSpace(parts[1])
161 case "Episode Info":
162 // Split the line, which is of the form: |SxE^Name^AirDate|.
163 parts = strings.SplitN(parts[1], "^", 3)
164 info.episodeName = parts[1]
165 // Split the episode string.
166 episode := strings.SplitN(parts[0], "x", 2)
167 info.episode.season, info.episode.episode = convertEpisode(episode[0], episode[1])
168 if info.episode.season == 0 && info.episode.season == info.episode.episode {
169 return nil
170 }
171 }
172 }
173 return &info
174 }