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