iTunes-Connect - Sales-Reports

Wer Apple und iTunes-Connect (ITC) kennt, weiß von den Problemchen welche ITC teilweise mit sich bringt. Mich persönlich hat immer gestört, das die Sales-Reports / Verkaufszahlen des Vortages manchmal erst am späten Abend des Folgetages verfügbar waren. Das liegt wohl daran, das die Apple-Server die Reports für jeden Entwickler aufbereiten müssen und es hier öfter mal zu Engpässen an Rechenressourcen kommt. Das ist natürlich nur eine Vermutung meinerseits...Wie dem auch sei! ;-)

itc_umsatz_rechnerIch habe mir vor ca. einem Jahr ein kleines Tool in Java namens "ITC - Umsatz-Rechner" (ja ja, ich weiß, ziemlich einfallsreich ;) ) geschrieben, welches die Roh-Daten der Verkäufe für einen beliebigen Zeitraum mit Hilfe der "Autoingestion.class" (Download über Entwickler-Account bei Apple) herunter lädt und diese Daten auswertet. Heraus kommt dann eine HTML-Datei, welche alle Verkaufszahlen aller Apps des gewählten Zeitraumes enthält. Häufig vorkommende Währungen wie USD, GBP, AUD, CAD und z.B. CHF werden anhand eines hinterlegten Wechselkurses direkt in Euro (EUR) umgerechnet. Des weiteren werden Zusammenfassung für die einzelnen Länder erstellt, woraus hervor geht, in welchem Land wieviele Neuinstallationen, Updates oder InApp-Käufe getätigt wurden. Das Ganze dann nochmal separiert nach iPhone, iPad oder Mac.

ITC - Umsatz-Rechner - Funktionsweise

Zunächst holen wir uns eine Kalendar-Instanz damit wir Apples Autoingestion.class mitteilen können, welchen Zeitraum oder welches Datum wir gerne hätten.

Calendar cal = Calendar.getInstance();
String sDateType, sDateFormat;

if (!bLoadTagesansicht){
//monatlich
sDateType = "Monthly";
sDateFormat = "yyyyMM";
cal.add(Calendar.MONTH, -1);
}else{
//täglich
sDateType = "Daily";
sDateFormat = "yyyyMMdd";
cal.add(Calendar.DATE, iHeuteMinusXTage);
/*
//jährlich
sDateType = "Yearly";
sDateFormat = "yyyy";
cal.add(Calendar.YEAR, -1);
*/
}

String sDate = new SimpleDateFormat(sDateFormat).format(cal.getTime());
String sDateTypeShort = sDateType.substring(0, 1);

Jetzt bilden wir die entsprechende Befehlszeile für die Autoingestion.class und rufen diese mittels exec() auf und warten 2-3 Sekunden bis der Download abgeschlossen ist.

String sCmd = "java Autoingestion"
+ " " + sUser
+ " " + sPass
+ " " + sVendorID
+ " Sales "
+ sDateType
+ " Summary"
+ " " + sDate;

Process proc = Runtime.getRuntime().exec(sCmd);

InputStream inputStream = proc.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

proc.waitFor();

String line;
while ((line = bufferedReader.readLine()) != null)
{
//echo(line);
}

Nachdem die Roh-Daten aus dem ITC-Account herunter geladen wurden, kann anhand des gewählten Zeitraumes und der Vendor-ID der Dateiname der herunter geladenen Datei gebildet werden. Diese ist mit GZip komprimiert und wird daher im nächsten Step entpackt, und im Anschluß geparsed bzw. verarbeitet. Hinweis: Die verwendeten Hilfsklassen wie Downloader.java und myAppUmsatzEntry.java werden weiter unten näher erläutert. Ist an dieser Stelle erstmal nicht ganz so wichtig.

String sFile = "S_" + sDateTypeShort + "_" + sVendorID + "_" + sDate + ".txt.gz";
//echo("Suche " + sFile + "...");

String sDataFile = "data.txt";
if (downloader.unzip(sFile, sDataFile)){
calculate(sDataFile, sSaveAs);
}else{
//echo("Daten fuer " + sDate + " liegen noch nicht vor. :/");
}

Die Funktion calculate() ließt die Datei mit den Rohdaten aus ITC ein, öffnet ein neues HTML-Dokument und wandelt die Daten kurz gesagt in etwas schöneres, aber vor allem leserlicheres um. Zunächst wird eine ArrayList erstellt, in welche die einzelnen Daten geschrieben werden.

ArrayList<myAppUmsatzEntry> umsaetze = new ArrayList<myAppUmsatzEntry>();

Dann wird die Datei mit den Rohdaten eingelesen, die entsprechenden Spalten und Daten-Abschnitte zugeordnet und im Array zwischengespeichert.

FileReader fr = new FileReader(sFile);
BufferedReader br = new BufferedReader(fr);

String[] arr;
String s = br.readLine(); //erste zeile überspringen//head
while ((s = br.readLine())!=null)
{
arr = s.split("\t");
if (arr.length>=20)
{
myAppUmsatzEntry umsatzEntry = new myAppUmsatzEntry();

umsatzEntry.sSKU = arr[2];
umsatzEntry.sVersion = arr[5];
umsatzEntry.sType = arr[6];
umsatzEntry.iMenge = Integer.parseInt(arr[7]);
umsatzEntry.fGewinn = Float.parseFloat(arr[8]);
umsatzEntry.sWaehrung = arr[11];
umsatzEntry.sLand = arr[12];

if (sDatumVon.equalsIgnoreCase(""))
{
sDatumVon = arr[9];
sDatumBis = arr[10];

if (sDatumVon.equalsIgnoreCase(sDatumBis))
bIsTagesansicht = true;

downloader.echo("Statistik: " + sDatumVon + " bis " + sDatumBis + "\n");
}

//zwischenspeichern
umsaetze.add(umsatzEntry);
}
}
br.close();

Jetzt wo alle Daten formatiert im Arbeitsspeicher bzw. der ArrayList liegen, sortieren wir diese noch nach der SKU, damit alle Einträge mit der jeweiligen App-Kennung zusammen liegen.

Collections.sort(umsaetze, new SortBySku());

Nun wird eine neue HTML-Datei geöffnet, und als erstes eine Überschrift definiert, welches das aktuelle Datum bzw. den gewählten Zeitraum der Daten beinhaltet.

String sOutputFile = sSaveAs; //sDatumVon + "_bis_" + sDatumBis + ".html";
sOutputFile = sOutputFile.replace("/", "_");

PrintWriter pWriter = new PrintWriter(new FileWriter(sOutputFile), true);
pWriter.println(getHtmlStart() + "<h1>" + sDatumVon + " bis " + sDatumBis + "</h1>" + nl + nl);

Nun werden die einzelnen Einträge der ArrayList abgearbeitet, und der entsprechende Output erzeugt. Hier werden die Wechselkurse mit den Verkäufen in den entsprechenden Ländern verrechnet und formatiert ausgegeben.

.....................

pWriter.println(nl + "</div><b>" + sLastSKU + " - Installationen: " + String.valueOf(iInstallLastSku) + " - Updates: " + String.valueOf(iUpdateLastSku) + "</b>" + nl
+ "Gewinn:"
+ " " + downloader.getPreisFormated(fEuroLastSku) + " EUR"
+ " + " + downloader.getPreisFormated(fUsdLastSku) + " USD"
+ " + " + downloader.getPreisFormated(fGbpLastSku) + " GBP"
+ " + " + downloader.getPreisFormated(fAudLastSku) + " AUD"
+ " + " + downloader.getPreisFormated(fCadLastSku) + " CAD"
+ " + " + downloader.getPreisFormated(fChfLastSku) + " CHF"
+ " + X (andere W&auml;hrungen)"
+ nl);

float fTmp = fEuroLastSku
+ (fUsdLastSku * getWechselkurs("usd"))
+ (fGbpLastSku * getWechselkurs("gbp"))
+ (fAudLastSku * getWechselkurs("aud"))
+ (fCadLastSku * getWechselkurs("cad"))
+ (fChfLastSku * getWechselkurs("chf"));
pWriter.println("<h3>Gewinn EUR - kalkuliert: " + downloader.getPreisFormated(fTmp) + " EUR</h3>" + nl);

.....................

An dieser Stelle spare ich mir einige Code-Abschnitte, einfach um diese Seite hier nicht noch weiter zu flooten :D Den gesamten Code als Download gibt es weiter unten auf der Seite.

Helferklasse - Downloader.java

Die Klasse Downloader enthält diverse Hilfsfunktionen. Unter anderem auch Download- und Rundungsfunktionen. Zum entpacken des GZip-Archives nutze ich eine Funktion namens unzip(String sFile, String sSaveAS). Der erste Parameter ist der Pfad zum GZip-Archiv, und der zweite Parameter beinhaltet den Pfad bzw. Dateinamen, unter welchem die entpackte Datei gespeichert werden soll.

public static boolean unzip(String sFile, String sSaveAS)
{
try
{
File f = new File(sFile);
if (!f.exists())
return false;

GZIPInputStream gzipInputStream = null;
gzipInputStream = new GZIPInputStream(new FileInputStream(sFile));
OutputStream out = new FileOutputStream(sSaveAS);

byte[] buf = new byte[1024];
int len;
while ((len = gzipInputStream.read(buf)) > 0) {
out.write(buf, 0, len);
}
gzipInputStream.close();
out.close();
f.delete();

return true;
}
catch (Exception ex)
{
echo("error unzip: " + ex.getLocalizedMessage());
}
return false;
}

Helferklasse - myAppUmsatzEntry.java

Die Klasse myAppUmsatzEntry ist eine Struktur die Informationen zur App, Version, dem entsprechenden Land, Anzahl Verkäufe bzw. Downloads, dem Gewinn und einigem mehr enthält. Neben dem Speichern von Informationen gibt es auch noch eine Funktion namens getTypeAsString() welche anhand des Type-Codes des Eintrages einen lesbaren String zurück gibt. So muss man nicht schauen das 7T ein Update auf iPad war. Hier die Funktion auf einen Blick, sollte selbsterklärend sein. ;-)

public String getTypeAsString()
{
if (sType.equalsIgnoreCase("1"))
return "Free/paid";
else if (sType.equalsIgnoreCase("7"))
return "Update";
else if (sType.equalsIgnoreCase("IA1") || sType.equalsIgnoreCase("IA9") || sType.equalsIgnoreCase("IAY") || sType.equalsIgnoreCase("IAC"))
return "InApp";
else if (sType.equalsIgnoreCase("1F"))
return "Free/paid";
else if (sType.equalsIgnoreCase("7F"))
return "Update";
else if (sType.equalsIgnoreCase("1T"))
return "Free/paid (iPad)";
else if (sType.equalsIgnoreCase("7T"))
return "Update (iPad)";
else if (sType.equalsIgnoreCase("F1"))
return "Free/paid (Mac)";
else if (sType.equalsIgnoreCase("F7"))
return "Update (Mac)";
else if (sType.equalsIgnoreCase("FI1"))
return "InApp (Mac)";
else if (sType.equalsIgnoreCase("1E"))
return "Paid (iPhone/iPod)";
else if (sType.equalsIgnoreCase("1EP"))
return "Paid (iPad)";
else if (sType.equalsIgnoreCase("1EU"))
return "Paid (Universal)";
else
return "Unknown";
}

Schlußwort und Download-Links zum Code

Die Idee hinter diesem Tool ist simpel, die Umsetzung relativ gut gelungen. Da das Tool nebenbei und auf die schnelle geschrieben wurde, könnte hier und da der Code sicher noch etwas optimiert werden, aber alles in allem Funktioniert es wie es soll.

Hier gibt es die entsprechenden Klassen und den sonst noch benötigten Code zum Download.
Viel Spaß damit! :-)

Bei Fragen, Anmerkungen oder Erweiterungsvorschlägen nutze bitte das Kontaktformular.

Zuletzt bearbeitet: 2. Januar, 2015
Tags: , , , , ,

Verwandte Beiträge

Der Apfel-Appstore-Reader

Aufgrund der Tatsache das Apple sich bezüglich der App-Rankings (Positionierung im Appstore) sehr gedeckt hält, und auch im Report-Tool (ITC / iTunes-Connect -> Sales & Trends) keinerlei Informationen auftauchen, an welcher Position man sich mit der jeweiligen App in z.B. einer Kategorie wie "Dienstprogramme" befindet, habe ich ein kleines Java-Tool (mit dem einfallsreichen Name Appstore-Reader) [...] Weiterlesen »


DIYS Smart-Home V1 - Irgendwo muss man ja anfangen!

Schon seit einiger Zeit plane ich mein Smart-Home. Neben dem Kostenfaktor für all die tollen Dinge die ich mir so vorstelle fehlt es oft an der notwendigen Zeit zur detailierten Planung und Umsetzung. Daher habe ich beschlossen einfach mal irgendwo anzufangen und dieses DIY-Projekt parallel niederzuschreiben...mal sehen wohin das führt...Ich bitte um Nachsicht wenn ich [...] Weiterlesen »


Telegram Messenger und die Bot API - Teil 2

Im ersten Teil - Telegram Messenger und die Bot API - bin ich auf einige Grundlagen eingegangen wie man einen Telegram-Bot einrichtet und diesen nutzt um sich Nachrichten, Informationen und sonstiges bequem auf sein Handy zu schicken. Da sich dieses Werkzeug als äußerst praktisch erwiesen hat, habe ich mich dazu entschlossen den vorherigen Beitrag nochmal [...] Weiterlesen »


HTAccess-Generator

Erstelle dir hier schnell und einfach einen HTAccess-Verzeichnisschutz für deine Web-/Internetseite. So kannst du den Hypertext-Zugriff auf Verzeichnisse deiner Webseite vor unberechtigten Zugriffen schützen. Der jeweilige Benutzer muss so zunächst die Benutzerkennung und das entsprechende Passwort eingeben bevor er den Inhalt zu Gesicht bekommt. Zum Beispiel lässt sich so auch ein CMS (Content-Management-Systeme wie z.B. [...] Weiterlesen »


Passwort-Generator

Wer kennt es nicht, man benötigt ein neues Passwort, und es fällt einem nichts ein. Zumindest nichts, was man nicht schon in der Vergangenheit in irgend einer Form bereits benutzt hätte. Heutzutage ist es auch nicht mehr einfach mit einem Passwort ala "liebe123" oder "lassmichrein" getan. Gerade Server-Administratoren aber auch Privat-Personen sollten mehr Wert auf [...] Weiterlesen »